From 30bb15666dc87d5ddff1fde785fb5e7390ffa389 Mon Sep 17 00:00:00 2001 From: smartDev420 Date: Fri, 28 Jun 2019 10:59:04 -0700 Subject: [PATCH] [change] Remove ListView Ref #1352 Ref https://github.com/facebook/react-native/issues/23313 --- README.md | 1 - .../src/moduleMap.js | 1 - .../src/exports/ListView/index.js | 12 - packages/react-native-web/src/index.js | 2 - .../ListView/ListViewDataSource.js | 426 ---------- .../ListView/cloneReferencedElement.js | 34 - .../src/vendor/react-native/ListView/index.js | 762 ------------------ 7 files changed, 1238 deletions(-) delete mode 100644 packages/react-native-web/src/exports/ListView/index.js delete mode 100644 packages/react-native-web/src/vendor/react-native/ListView/ListViewDataSource.js delete mode 100644 packages/react-native-web/src/vendor/react-native/ListView/cloneReferencedElement.js delete mode 100644 packages/react-native-web/src/vendor/react-native/ListView/index.js diff --git a/README.md b/README.md index 33f0331b..40b638b5 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,6 @@ React Native v0.55 | Image | ✓ | Missing multiple sources ([#515](https://github.com/necolas/react-native-web/issues/515)) and HTTP headers ([#1019](https://github.com/necolas/react-native-web/issues/1019)). | | ImageBackground | ✓ | | | KeyboardAvoidingView | (✓) | Mock. No equivalent web APIs. | -| ListView | ✓ | | | Modal | ✘ | Not started ([#1020](https://github.com/necolas/react-native-web/issues/1020)). | | Picker | ✓ | | | RefreshControl | ✘ | Not started ([#1027](https://github.com/necolas/react-native-web/issues/1027)). | diff --git a/packages/babel-plugin-react-native-web/src/moduleMap.js b/packages/babel-plugin-react-native-web/src/moduleMap.js index 2e78d2db..06d57f8f 100644 --- a/packages/babel-plugin-react-native-web/src/moduleMap.js +++ b/packages/babel-plugin-react-native-web/src/moduleMap.js @@ -27,7 +27,6 @@ module.exports = { KeyboardAvoidingView: true, LayoutAnimation: true, Linking: true, - ListView: true, MaskedViewIOS: true, Modal: true, NativeEventEmitter: true, diff --git a/packages/react-native-web/src/exports/ListView/index.js b/packages/react-native-web/src/exports/ListView/index.js deleted file mode 100644 index 7788f6d4..00000000 --- a/packages/react-native-web/src/exports/ListView/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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 ListView from '../../vendor/react-native/ListView'; -export default ListView; diff --git a/packages/react-native-web/src/index.js b/packages/react-native-web/src/index.js index 12de1b43..31cbafa6 100644 --- a/packages/react-native-web/src/index.js +++ b/packages/react-native-web/src/index.js @@ -41,7 +41,6 @@ import FlatList from './exports/FlatList'; import Image from './exports/Image'; import ImageBackground from './exports/ImageBackground'; import KeyboardAvoidingView from './exports/KeyboardAvoidingView'; -import ListView from './exports/ListView'; import Modal from './exports/Modal'; import Picker from './exports/Picker'; import ProgressBar from './exports/ProgressBar'; @@ -142,7 +141,6 @@ export { Image, ImageBackground, KeyboardAvoidingView, - ListView, Modal, Picker, ProgressBar, diff --git a/packages/react-native-web/src/vendor/react-native/ListView/ListViewDataSource.js b/packages/react-native-web/src/vendor/react-native/ListView/ListViewDataSource.js deleted file mode 100644 index 5b7e84c6..00000000 --- a/packages/react-native-web/src/vendor/react-native/ListView/ListViewDataSource.js +++ /dev/null @@ -1,426 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @providesModule ListViewDataSource - * @flow - * @format - */ -'use strict'; - -import invariant from 'fbjs/lib/invariant'; -import isEmpty from '../isEmpty'; -import warning from 'fbjs/lib/warning'; - -function defaultGetRowData( - dataBlob: any, - sectionID: number | string, - rowID: number | string, -): any { - return dataBlob[sectionID][rowID]; -} - -function defaultGetSectionHeaderData( - dataBlob: any, - sectionID: number | string, -): any { - return dataBlob[sectionID]; -} - -type differType = (data1: any, data2: any) => boolean; - -type ParamType = { - rowHasChanged: differType, - getRowData?: ?typeof defaultGetRowData, - sectionHeaderHasChanged?: ?differType, - getSectionHeaderData?: ?typeof defaultGetSectionHeaderData, -}; - -/** - * Provides efficient data processing and access to the - * `ListView` component. A `ListViewDataSource` is created with functions for - * extracting data from the input blob, and comparing elements (with default - * implementations for convenience). The input blob can be as simple as an - * array of strings, or an object with rows nested inside section objects. - * - * To update the data in the datasource, use `cloneWithRows` (or - * `cloneWithRowsAndSections` if you care about sections). The data in the - * data source is immutable, so you can't modify it directly. The clone methods - * suck in the new data and compute a diff for each row so ListView knows - * whether to re-render it or not. - * - * In this example, a component receives data in chunks, handled by - * `_onDataArrived`, which concats the new data onto the old data and updates the - * data source. We use `concat` to create a new array - mutating `this._data`, - * e.g. with `this._data.push(newRowData)`, would be an error. `_rowHasChanged` - * understands the shape of the row data and knows how to efficiently compare - * it. - * - * ``` - * getInitialState: function() { - * var ds = new ListView.DataSource({rowHasChanged: this._rowHasChanged}); - * return {ds}; - * }, - * _onDataArrived(newData) { - * this._data = this._data.concat(newData); - * this.setState({ - * ds: this.state.ds.cloneWithRows(this._data) - * }); - * } - * ``` - */ - -class ListViewDataSource { - /** - * You can provide custom extraction and `hasChanged` functions for section - * headers and rows. If absent, data will be extracted with the - * `defaultGetRowData` and `defaultGetSectionHeaderData` functions. - * - * The default extractor expects data of one of the following forms: - * - * { sectionID_1: { rowID_1: , ... }, ... } - * - * or - * - * { sectionID_1: [ , , ... ], ... } - * - * or - * - * [ [ , , ... ], ... ] - * - * The constructor takes in a params argument that can contain any of the - * following: - * - * - getRowData(dataBlob, sectionID, rowID); - * - getSectionHeaderData(dataBlob, sectionID); - * - rowHasChanged(prevRowData, nextRowData); - * - sectionHeaderHasChanged(prevSectionData, nextSectionData); - */ - constructor(params: ParamType) { - invariant( - params && typeof params.rowHasChanged === 'function', - 'Must provide a rowHasChanged function.', - ); - this._rowHasChanged = params.rowHasChanged; - this._getRowData = params.getRowData || defaultGetRowData; - this._sectionHeaderHasChanged = params.sectionHeaderHasChanged; - this._getSectionHeaderData = - params.getSectionHeaderData || defaultGetSectionHeaderData; - - this._dataBlob = null; - this._dirtyRows = []; - this._dirtySections = []; - this._cachedRowCount = 0; - - // These two private variables are accessed by outsiders because ListView - // uses them to iterate over the data in this class. - this.rowIdentities = []; - this.sectionIdentities = []; - } - - /** - * Clones this `ListViewDataSource` with the specified `dataBlob` and - * `rowIdentities`. The `dataBlob` is just an arbitrary blob of data. At - * construction an extractor to get the interesting information was defined - * (or the default was used). - * - * The `rowIdentities` is a 2D array of identifiers for rows. - * ie. [['a1', 'a2'], ['b1', 'b2', 'b3'], ...]. If not provided, it's - * assumed that the keys of the section data are the row identities. - * - * Note: This function does NOT clone the data in this data source. It simply - * passes the functions defined at construction to a new data source with - * the data specified. If you wish to maintain the existing data you must - * handle merging of old and new data separately and then pass that into - * this function as the `dataBlob`. - */ - cloneWithRows( - dataBlob: $ReadOnlyArray | {+[key: string]: any}, - rowIdentities: ?$ReadOnlyArray, - ): ListViewDataSource { - var rowIds = rowIdentities ? [[...rowIdentities]] : null; - if (!this._sectionHeaderHasChanged) { - this._sectionHeaderHasChanged = () => false; - } - return this.cloneWithRowsAndSections({s1: dataBlob}, ['s1'], rowIds); - } - - /** - * This performs the same function as the `cloneWithRows` function but here - * you also specify what your `sectionIdentities` are. If you don't care - * about sections you should safely be able to use `cloneWithRows`. - * - * `sectionIdentities` is an array of identifiers for sections. - * ie. ['s1', 's2', ...]. The identifiers should correspond to the keys or array indexes - * of the data you wish to include. If not provided, it's assumed that the - * keys of dataBlob are the section identities. - * - * Note: this returns a new object! - * - * ``` - * const dataSource = ds.cloneWithRowsAndSections({ - * addresses: ['row 1', 'row 2'], - * phone_numbers: ['data 1', 'data 2'], - * }, ['phone_numbers']); - * ``` - */ - cloneWithRowsAndSections( - dataBlob: any, - sectionIdentities: ?Array, - rowIdentities: ?Array>, - ): ListViewDataSource { - invariant( - typeof this._sectionHeaderHasChanged === 'function', - 'Must provide a sectionHeaderHasChanged function with section data.', - ); - invariant( - !sectionIdentities || - !rowIdentities || - sectionIdentities.length === rowIdentities.length, - 'row and section ids lengths must be the same', - ); - - var newSource = new ListViewDataSource({ - getRowData: this._getRowData, - getSectionHeaderData: this._getSectionHeaderData, - rowHasChanged: this._rowHasChanged, - sectionHeaderHasChanged: this._sectionHeaderHasChanged, - }); - newSource._dataBlob = dataBlob; - if (sectionIdentities) { - newSource.sectionIdentities = sectionIdentities; - } else { - newSource.sectionIdentities = Object.keys(dataBlob); - } - if (rowIdentities) { - newSource.rowIdentities = rowIdentities; - } else { - newSource.rowIdentities = []; - newSource.sectionIdentities.forEach(sectionID => { - newSource.rowIdentities.push(Object.keys(dataBlob[sectionID])); - }); - } - newSource._cachedRowCount = countRows(newSource.rowIdentities); - - newSource._calculateDirtyArrays( - this._dataBlob, - this.sectionIdentities, - this.rowIdentities, - ); - - return newSource; - } - - /** - * Returns the total number of rows in the data source. - * - * If you are specifying the rowIdentities or sectionIdentities, then `getRowCount` will return the number of rows in the filtered data source. - */ - getRowCount(): number { - return this._cachedRowCount; - } - - /** - * Returns the total number of rows in the data source (see `getRowCount` for how this is calculated) plus the number of sections in the data. - * - * If you are specifying the rowIdentities or sectionIdentities, then `getRowAndSectionCount` will return the number of rows & sections in the filtered data source. - */ - getRowAndSectionCount(): number { - return this._cachedRowCount + this.sectionIdentities.length; - } - - /** - * Returns if the row is dirtied and needs to be rerendered - */ - rowShouldUpdate(sectionIndex: number, rowIndex: number): boolean { - var needsUpdate = this._dirtyRows[sectionIndex][rowIndex]; - warning( - needsUpdate !== undefined, - 'missing dirtyBit for section, row: ' + sectionIndex + ', ' + rowIndex, - ); - return needsUpdate; - } - - /** - * Gets the data required to render the row. - */ - getRowData(sectionIndex: number, rowIndex: number): any { - var sectionID = this.sectionIdentities[sectionIndex]; - var rowID = this.rowIdentities[sectionIndex][rowIndex]; - warning( - sectionID !== undefined && rowID !== undefined, - 'rendering invalid section, row: ' + sectionIndex + ', ' + rowIndex, - ); - return this._getRowData(this._dataBlob, sectionID, rowID); - } - - /** - * Gets the rowID at index provided if the dataSource arrays were flattened, - * or null of out of range indexes. - */ - getRowIDForFlatIndex(index: number): ?string { - var accessIndex = index; - for (var ii = 0; ii < this.sectionIdentities.length; ii++) { - if (accessIndex >= this.rowIdentities[ii].length) { - accessIndex -= this.rowIdentities[ii].length; - } else { - return this.rowIdentities[ii][accessIndex]; - } - } - return null; - } - - /** - * Gets the sectionID at index provided if the dataSource arrays were flattened, - * or null for out of range indexes. - */ - getSectionIDForFlatIndex(index: number): ?string { - var accessIndex = index; - for (var ii = 0; ii < this.sectionIdentities.length; ii++) { - if (accessIndex >= this.rowIdentities[ii].length) { - accessIndex -= this.rowIdentities[ii].length; - } else { - return this.sectionIdentities[ii]; - } - } - return null; - } - - /** - * Returns an array containing the number of rows in each section - */ - getSectionLengths(): Array { - var results = []; - for (var ii = 0; ii < this.sectionIdentities.length; ii++) { - results.push(this.rowIdentities[ii].length); - } - return results; - } - - /** - * Returns if the section header is dirtied and needs to be rerendered - */ - sectionHeaderShouldUpdate(sectionIndex: number): boolean { - var needsUpdate = this._dirtySections[sectionIndex]; - warning( - needsUpdate !== undefined, - 'missing dirtyBit for section: ' + sectionIndex, - ); - return needsUpdate; - } - - /** - * Gets the data required to render the section header - */ - getSectionHeaderData(sectionIndex: number): any { - if (!this._getSectionHeaderData) { - return null; - } - var sectionID = this.sectionIdentities[sectionIndex]; - warning( - sectionID !== undefined, - 'renderSection called on invalid section: ' + sectionIndex, - ); - return this._getSectionHeaderData(this._dataBlob, sectionID); - } - - /** - * Private members and methods. - */ - - _getRowData: typeof defaultGetRowData; - _getSectionHeaderData: typeof defaultGetSectionHeaderData; - _rowHasChanged: differType; - _sectionHeaderHasChanged: ?differType; - - _dataBlob: any; - _dirtyRows: Array>; - _dirtySections: Array; - _cachedRowCount: number; - - // These two 'protected' variables are accessed by ListView to iterate over - // the data in this class. - rowIdentities: Array>; - sectionIdentities: Array; - - _calculateDirtyArrays( - prevDataBlob: any, - prevSectionIDs: Array, - prevRowIDs: Array>, - ): void { - // construct a hashmap of the existing (old) id arrays - var prevSectionsHash = keyedDictionaryFromArray(prevSectionIDs); - var prevRowsHash = {}; - for (var ii = 0; ii < prevRowIDs.length; ii++) { - var sectionID = prevSectionIDs[ii]; - warning( - !prevRowsHash[sectionID], - 'SectionID appears more than once: ' + sectionID, - ); - prevRowsHash[sectionID] = keyedDictionaryFromArray(prevRowIDs[ii]); - } - - // compare the 2 identity array and get the dirtied rows - this._dirtySections = []; - this._dirtyRows = []; - - var dirty; - for (var sIndex = 0; sIndex < this.sectionIdentities.length; sIndex++) { - var sectionID = this.sectionIdentities[sIndex]; - // dirty if the sectionHeader is new or _sectionHasChanged is true - dirty = !prevSectionsHash[sectionID]; - var sectionHeaderHasChanged = this._sectionHeaderHasChanged; - if (!dirty && sectionHeaderHasChanged) { - dirty = sectionHeaderHasChanged( - this._getSectionHeaderData(prevDataBlob, sectionID), - this._getSectionHeaderData(this._dataBlob, sectionID), - ); - } - this._dirtySections.push(!!dirty); - - this._dirtyRows[sIndex] = []; - for ( - var rIndex = 0; - rIndex < this.rowIdentities[sIndex].length; - rIndex++ - ) { - var rowID = this.rowIdentities[sIndex][rIndex]; - // dirty if the section is new, row is new or _rowHasChanged is true - dirty = - !prevSectionsHash[sectionID] || - !prevRowsHash[sectionID][rowID] || - this._rowHasChanged( - this._getRowData(prevDataBlob, sectionID, rowID), - this._getRowData(this._dataBlob, sectionID, rowID), - ); - this._dirtyRows[sIndex].push(!!dirty); - } - } - } -} - -function countRows(allRowIDs) { - var totalRows = 0; - for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) { - var rowIDs = allRowIDs[sectionIdx]; - totalRows += rowIDs.length; - } - return totalRows; -} - -function keyedDictionaryFromArray(arr) { - if (isEmpty(arr)) { - return {}; - } - var result = {}; - for (var ii = 0; ii < arr.length; ii++) { - var key = arr[ii]; - warning(!result[key], 'Value appears more than once in array: ' + key); - result[key] = true; - } - return result; -} - -export default ListViewDataSource; diff --git a/packages/react-native-web/src/vendor/react-native/ListView/cloneReferencedElement.js b/packages/react-native-web/src/vendor/react-native/ListView/cloneReferencedElement.js deleted file mode 100644 index 93267ff2..00000000 --- a/packages/react-native-web/src/vendor/react-native/ListView/cloneReferencedElement.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -import React from 'react'; - -const __DEV__ = process.env.NODE_ENV !== 'production'; - -function cloneReferencedElement(element, config, ...children) { - let cloneRef = config.ref; - let originalRef = element.ref; - if (originalRef == null || cloneRef == null) { - return React.cloneElement(element, config, ...children); - } - - if (typeof originalRef !== 'function') { - if (__DEV__) { - console.warn( - 'Cloning an element with a ref that will be overwritten because it ' + - 'is not a function. Use a composable callback-style ref instead. ' + - 'Ignoring ref: ' + originalRef, - ); - } - return React.cloneElement(element, config, ...children); - } - - return React.cloneElement(element, { - ...config, - ref(component) { - cloneRef(component); - originalRef(component); - }, - }, ...children); -} - -export default cloneReferencedElement; diff --git a/packages/react-native-web/src/vendor/react-native/ListView/index.js b/packages/react-native-web/src/vendor/react-native/ListView/index.js deleted file mode 100644 index c339af50..00000000 --- a/packages/react-native-web/src/vendor/react-native/ListView/index.js +++ /dev/null @@ -1,762 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @providesModule ListView - * @flow - * @format - */ -'use strict'; - -import ListViewDataSource from './ListViewDataSource'; -import Platform from '../../../exports/Platform'; -import React from 'react'; -import PropTypes from 'prop-types'; -import findNodeHandle from '../../../exports/findNodeHandle'; -import NativeModules from '../../../exports/NativeModules'; -import ScrollView from '../../../exports/ScrollView'; -import ScrollResponder from '../../../modules/ScrollResponder'; -import StaticRenderer from '../StaticRenderer'; -import TimerMixin from 'react-timer-mixin'; -import View from '../../../exports/View'; - -import cloneReferencedElement from './cloneReferencedElement'; -import createReactClass from 'create-react-class'; -import isEmpty from '../isEmpty'; - -var merge = (...args) => Object.assign({}, ...args); -var RCTScrollViewManager = NativeModules.ScrollViewManager; - -var DEFAULT_PAGE_SIZE = 1; -var DEFAULT_INITIAL_ROWS = 10; -var DEFAULT_SCROLL_RENDER_AHEAD = 1000; -var DEFAULT_END_REACHED_THRESHOLD = 1000; -var DEFAULT_SCROLL_CALLBACK_THROTTLE = 50; - -/** - * DEPRECATED - use one of the new list components, such as [`FlatList`](docs/flatlist.html) - * or [`SectionList`](docs/sectionlist.html) for bounded memory use, fewer bugs, - * better performance, an easier to use API, and more features. Check out this - * [blog post](https://facebook.github.io/react-native/blog/2017/03/13/better-list-views.html) - * for more details. - * - * ListView - A core component designed for efficient display of vertically - * scrolling lists of changing data. The minimal API is to create a - * [`ListView.DataSource`](docs/listviewdatasource.html), populate it with a simple - * array of data blobs, and instantiate a `ListView` component with that data - * source and a `renderRow` callback which takes a blob from the data array and - * returns a renderable component. - * - * Minimal example: - * - * ``` - * class MyComponent extends Component { - * constructor() { - * super(); - * const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); - * this.state = { - * dataSource: ds.cloneWithRows(['row 1', 'row 2']), - * }; - * } - * - * render() { - * return ( - * {rowData}} - * /> - * ); - * } - * } - * ``` - * - * ListView also supports more advanced features, including sections with sticky - * section headers, header and footer support, callbacks on reaching the end of - * the available data (`onEndReached`) and on the set of rows that are visible - * in the device viewport change (`onChangeVisibleRows`), and several - * performance optimizations. - * - * There are a few performance operations designed to make ListView scroll - * smoothly while dynamically loading potentially very large (or conceptually - * infinite) data sets: - * - * * Only re-render changed rows - the rowHasChanged function provided to the - * data source tells the ListView if it needs to re-render a row because the - * source data has changed - see ListViewDataSource for more details. - * - * * Rate-limited row rendering - By default, only one row is rendered per - * event-loop (customizable with the `pageSize` prop). This breaks up the - * work into smaller chunks to reduce the chance of dropping frames while - * rendering rows. - */ - -var ListView = createReactClass({ - displayName: 'ListView', - _childFrames: ([]: Array), - _sentEndForContentLength: (null: ?number), - _scrollComponent: (null: any), - _prevRenderedRowsCount: 0, - _visibleRows: ({}: Object), - scrollProperties: ({}: Object), - - mixins: [ScrollResponder.Mixin, TimerMixin], - - statics: { - DataSource: ListViewDataSource, - }, - - /** - * You must provide a renderRow function. If you omit any of the other render - * functions, ListView will simply skip rendering them. - * - * - renderRow(rowData, sectionID, rowID, highlightRow); - * - renderSectionHeader(sectionData, sectionID); - */ - propTypes: { - ...ScrollView.propTypes, - /** - * An instance of [ListView.DataSource](docs/listviewdatasource.html) to use - */ - dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired, - /** - * (sectionID, rowID, adjacentRowHighlighted) => renderable - * - * If provided, a renderable component to be rendered as the separator - * below each row but not the last row if there is a section header below. - * Take a sectionID and rowID of the row above and whether its adjacent row - * is highlighted. - */ - renderSeparator: PropTypes.func, - /** - * (rowData, sectionID, rowID, highlightRow) => renderable - * - * Takes a data entry from the data source and its ids and should return - * a renderable component to be rendered as the row. By default the data - * is exactly what was put into the data source, but it's also possible to - * provide custom extractors. ListView can be notified when a row is - * being highlighted by calling `highlightRow(sectionID, rowID)`. This - * sets a boolean value of adjacentRowHighlighted in renderSeparator, allowing you - * to control the separators above and below the highlighted row. The highlighted - * state of a row can be reset by calling highlightRow(null). - */ - renderRow: PropTypes.func.isRequired, - /** - * How many rows to render on initial component mount. Use this to make - * it so that the first screen worth of data appears at one time instead of - * over the course of multiple frames. - */ - initialListSize: PropTypes.number.isRequired, - /** - * Called when all rows have been rendered and the list has been scrolled - * to within onEndReachedThreshold of the bottom. The native scroll - * event is provided. - */ - onEndReached: PropTypes.func, - /** - * Threshold in pixels (virtual, not physical) for calling onEndReached. - */ - onEndReachedThreshold: PropTypes.number.isRequired, - /** - * Number of rows to render per event loop. Note: if your 'rows' are actually - * cells, i.e. they don't span the full width of your view (as in the - * ListViewGridLayoutExample), you should set the pageSize to be a multiple - * of the number of cells per row, otherwise you're likely to see gaps at - * the edge of the ListView as new pages are loaded. - */ - pageSize: PropTypes.number.isRequired, - /** - * () => renderable - * - * The header and footer are always rendered (if these props are provided) - * on every render pass. If they are expensive to re-render, wrap them - * in StaticContainer or other mechanism as appropriate. Footer is always - * at the bottom of the list, and header at the top, on every render pass. - * In a horizontal ListView, the header is rendered on the left and the - * footer on the right. - */ - renderFooter: PropTypes.func, - renderHeader: PropTypes.func, - /** - * (sectionData, sectionID) => renderable - * - * If provided, a header is rendered for this section. - */ - renderSectionHeader: PropTypes.func, - /** - * (props) => renderable - * - * A function that returns the scrollable component in which the list rows - * are rendered. Defaults to returning a ScrollView with the given props. - */ - renderScrollComponent: PropTypes.func.isRequired, - /** - * How early to start rendering rows before they come on screen, in - * pixels. - */ - scrollRenderAheadDistance: PropTypes.number.isRequired, - /** - * (visibleRows, changedRows) => void - * - * Called when the set of visible rows changes. `visibleRows` maps - * { sectionID: { rowID: true }} for all the visible rows, and - * `changedRows` maps { sectionID: { rowID: true | false }} for the rows - * that have changed their visibility, with true indicating visible, and - * false indicating the view has moved out of view. - */ - onChangeVisibleRows: PropTypes.func, - /** - * A performance optimization for improving scroll perf of - * large lists, used in conjunction with overflow: 'hidden' on the row - * containers. This is enabled by default. - */ - removeClippedSubviews: PropTypes.bool, - /** - * Makes the sections headers sticky. The sticky behavior means that it - * will scroll with the content at the top of the section until it reaches - * the top of the screen, at which point it will stick to the top until it - * is pushed off the screen by the next section header. This property is - * not supported in conjunction with `horizontal={true}`. Only enabled by - * default on iOS because of typical platform standards. - */ - stickySectionHeadersEnabled: PropTypes.bool, - /** - * An array of child indices determining which children get docked to the - * top of the screen when scrolling. For example, passing - * `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the - * top of the scroll view. This property is not supported in conjunction - * with `horizontal={true}`. - */ - stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number).isRequired, - /** - * Flag indicating whether empty section headers should be rendered. In the future release - * empty section headers will be rendered by default, and the flag will be deprecated. - * If empty sections are not desired to be rendered their indices should be excluded from sectionID object. - */ - enableEmptySections: PropTypes.bool, - }, - - /** - * Exports some data, e.g. for perf investigations or analytics. - */ - getMetrics: function() { - return { - contentLength: this.scrollProperties.contentLength, - totalRows: this.props.enableEmptySections - ? this.props.dataSource.getRowAndSectionCount() - : this.props.dataSource.getRowCount(), - renderedRows: this.state.curRenderedRowsCount, - visibleRows: Object.keys(this._visibleRows).length, - }; - }, - - /** - * Provides a handle to the underlying scroll responder. - * Note that `this._scrollComponent` might not be a `ScrollView`, so we - * need to check that it responds to `getScrollResponder` before calling it. - */ - getScrollResponder: function() { - if (this._scrollComponent && this._scrollComponent.getScrollResponder) { - return this._scrollComponent.getScrollResponder(); - } - }, - - getScrollableNode: function() { - if (this._scrollComponent && this._scrollComponent.getScrollableNode) { - return this._scrollComponent.getScrollableNode(); - } else { - return findNodeHandle(this._scrollComponent); - } - }, - - /** - * Scrolls to a given x, y offset, either immediately or with a smooth animation. - * - * See `ScrollView#scrollTo`. - */ - scrollTo: function(...args: Array) { - if (this._scrollComponent && this._scrollComponent.scrollTo) { - this._scrollComponent.scrollTo(...args); - } - }, - - /** - * If this is a vertical ListView scrolls to the bottom. - * If this is a horizontal ListView scrolls to the right. - * - * Use `scrollToEnd({animated: true})` for smooth animated scrolling, - * `scrollToEnd({animated: false})` for immediate scrolling. - * If no options are passed, `animated` defaults to true. - * - * See `ScrollView#scrollToEnd`. - */ - scrollToEnd: function(options?: ?{animated?: ?boolean}) { - if (this._scrollComponent) { - if (this._scrollComponent.scrollToEnd) { - this._scrollComponent.scrollToEnd(options); - } else { - console.warn( - 'The scroll component used by the ListView does not support ' + - 'scrollToEnd. Check the renderScrollComponent prop of your ListView.', - ); - } - } - }, - - /** - * Displays the scroll indicators momentarily. - * - * @platform ios - */ - flashScrollIndicators: function() { - if (this._scrollComponent && this._scrollComponent.flashScrollIndicators) { - this._scrollComponent.flashScrollIndicators(); - } - }, - - setNativeProps: function(props: Object) { - if (this._scrollComponent) { - this._scrollComponent.setNativeProps(props); - } - }, - - /** - * React life cycle hooks. - */ - - getDefaultProps: function() { - return { - initialListSize: DEFAULT_INITIAL_ROWS, - pageSize: DEFAULT_PAGE_SIZE, - renderScrollComponent: props => , - scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD, - onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD, - stickySectionHeadersEnabled: Platform.OS === 'ios' || Platform.OS === 'web', - stickyHeaderIndices: [], - }; - }, - - getInitialState: function() { - return { - curRenderedRowsCount: this.props.initialListSize, - highlightedRow: ({}: Object), - }; - }, - - getInnerViewNode: function() { - return this._scrollComponent.getInnerViewNode(); - }, - - UNSAFE_componentWillMount: function() { - // this data should never trigger a render pass, so don't put in state - this.scrollProperties = { - visibleLength: null, - contentLength: null, - offset: 0, - }; - this._childFrames = []; - this._visibleRows = {}; - this._prevRenderedRowsCount = 0; - this._sentEndForContentLength = null; - }, - - componentDidMount: function() { - // do this in animation frame until componentDidMount actually runs after - // the component is laid out - this.requestAnimationFrame(() => { - this._measureAndUpdateScrollProps(); - }); - }, - - UNSAFE_componentWillReceiveProps: function(nextProps: Object) { - if ( - this.props.dataSource !== nextProps.dataSource || - this.props.initialListSize !== nextProps.initialListSize - ) { - this.setState( - (state, props) => { - this._prevRenderedRowsCount = 0; - return { - curRenderedRowsCount: Math.min( - Math.max(state.curRenderedRowsCount, props.initialListSize), - props.enableEmptySections - ? props.dataSource.getRowAndSectionCount() - : props.dataSource.getRowCount(), - ), - }; - }, - () => this._renderMoreRowsIfNeeded(), - ); - } - }, - - componentDidUpdate: function() { - this.requestAnimationFrame(() => { - this._measureAndUpdateScrollProps(); - }); - }, - - _onRowHighlighted: function(sectionID: string, rowID: string) { - this.setState({highlightedRow: {sectionID, rowID}}); - }, - - render: function() { - var bodyComponents = []; - - var dataSource = this.props.dataSource; - var allRowIDs = dataSource.rowIdentities; - var rowCount = 0; - var stickySectionHeaderIndices = []; - - const {renderSectionHeader} = this.props; - - var header = this.props.renderHeader && this.props.renderHeader(); - var footer = this.props.renderFooter && this.props.renderFooter(); - var totalIndex = header ? 1 : 0; - - for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) { - var sectionID = dataSource.sectionIdentities[sectionIdx]; - var rowIDs = allRowIDs[sectionIdx]; - if (rowIDs.length === 0) { - if (this.props.enableEmptySections === undefined) { - /* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses - * an error found when Flow v0.54 was deployed. To see the error - * delete this comment and run Flow. */ - var warning = require('fbjs/lib/warning'); - warning( - false, - 'In next release empty section headers will be rendered.' + - " In this release you can use 'enableEmptySections' flag to render empty section headers.", - ); - continue; - } else { - var invariant = require('fbjs/lib/invariant'); - invariant( - this.props.enableEmptySections, - "In next release 'enableEmptySections' flag will be deprecated, empty section headers will always be rendered." + - ' If empty section headers are not desirable their indices should be excluded from sectionIDs object.' + - " In this release 'enableEmptySections' may only have value 'true' to allow empty section headers rendering.", - ); - } - } - - if (renderSectionHeader) { - const element = renderSectionHeader( - dataSource.getSectionHeaderData(sectionIdx), - sectionID, - ); - if (element) { - bodyComponents.push( - React.cloneElement(element, {key: 's_' + sectionID}), - ); - if (this.props.stickySectionHeadersEnabled) { - stickySectionHeaderIndices.push(totalIndex); - } - totalIndex++; - } - } - - for (var rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) { - var rowID = rowIDs[rowIdx]; - var comboID = sectionID + '_' + rowID; - var shouldUpdateRow = - rowCount >= this._prevRenderedRowsCount && - dataSource.rowShouldUpdate(sectionIdx, rowIdx); - var row = ( - - ); - bodyComponents.push(row); - totalIndex++; - - if ( - this.props.renderSeparator && - (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1) - ) { - var adjacentRowHighlighted = - this.state.highlightedRow.sectionID === sectionID && - (this.state.highlightedRow.rowID === rowID || - this.state.highlightedRow.rowID === rowIDs[rowIdx + 1]); - var separator = this.props.renderSeparator( - sectionID, - rowID, - adjacentRowHighlighted, - ); - if (separator) { - bodyComponents.push({separator}); - totalIndex++; - } - } - if (++rowCount === this.state.curRenderedRowsCount) { - break; - } - } - if (rowCount >= this.state.curRenderedRowsCount) { - break; - } - } - - var {renderScrollComponent, ...props} = this.props; - if (!props.scrollEventThrottle) { - props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE; - } - if (props.removeClippedSubviews === undefined) { - props.removeClippedSubviews = true; - } - Object.assign(props, { - onScroll: this._onScroll, - stickyHeaderIndices: this.props.stickyHeaderIndices.concat( - stickySectionHeaderIndices, - ), - - // Do not pass these events downstream to ScrollView since they will be - // registered in ListView's own ScrollResponder.Mixin - onKeyboardWillShow: undefined, - onKeyboardWillHide: undefined, - onKeyboardDidShow: undefined, - onKeyboardDidHide: undefined, - }); - - return cloneReferencedElement( - renderScrollComponent(props), - { - ref: this._setScrollComponentRef, - onContentSizeChange: this._onContentSizeChange, - onLayout: this._onLayout, - DEPRECATED_sendUpdatedChildFrames: - typeof props.onChangeVisibleRows !== undefined, - }, - header, - bodyComponents, - footer, - ); - }, - - /** - * Private methods - */ - - _measureAndUpdateScrollProps: function() { - var scrollComponent = this.getScrollResponder(); - if (!scrollComponent || !scrollComponent.getInnerViewNode) { - return; - } - - // RCTScrollViewManager.calculateChildFrames is not available on - // every platform - RCTScrollViewManager && - RCTScrollViewManager.calculateChildFrames && - RCTScrollViewManager.calculateChildFrames( - findNodeHandle(scrollComponent), - this._updateVisibleRows, - ); - }, - - _setScrollComponentRef: function(scrollComponent: Object) { - this._scrollComponent = scrollComponent; - }, - - _onContentSizeChange: function(width: number, height: number) { - var contentLength = !this.props.horizontal ? height : width; - if (contentLength !== this.scrollProperties.contentLength) { - this.scrollProperties.contentLength = contentLength; - this._updateVisibleRows(); - this._renderMoreRowsIfNeeded(); - } - this.props.onContentSizeChange && - this.props.onContentSizeChange(width, height); - }, - - _onLayout: function(event: Object) { - var {width, height} = event.nativeEvent.layout; - var visibleLength = !this.props.horizontal ? height : width; - if (visibleLength !== this.scrollProperties.visibleLength) { - this.scrollProperties.visibleLength = visibleLength; - this._updateVisibleRows(); - this._renderMoreRowsIfNeeded(); - } - this.props.onLayout && this.props.onLayout(event); - }, - - _maybeCallOnEndReached: function(event?: Object) { - if ( - this.props.onEndReached && - this.scrollProperties.contentLength !== this._sentEndForContentLength && - this._getDistanceFromEnd(this.scrollProperties) < - this.props.onEndReachedThreshold && - this.state.curRenderedRowsCount === - (this.props.enableEmptySections - ? this.props.dataSource.getRowAndSectionCount() - : this.props.dataSource.getRowCount()) - ) { - this._sentEndForContentLength = this.scrollProperties.contentLength; - this.props.onEndReached(event); - return true; - } - return false; - }, - - _renderMoreRowsIfNeeded: function() { - if ( - this.scrollProperties.contentLength === null || - this.scrollProperties.visibleLength === null || - this.state.curRenderedRowsCount === - (this.props.enableEmptySections - ? this.props.dataSource.getRowAndSectionCount() - : this.props.dataSource.getRowCount()) - ) { - this._maybeCallOnEndReached(); - return; - } - - var distanceFromEnd = this._getDistanceFromEnd(this.scrollProperties); - if (distanceFromEnd < this.props.scrollRenderAheadDistance) { - this._pageInNewRows(); - } - }, - - _pageInNewRows: function() { - this.setState( - (state, props) => { - var rowsToRender = Math.min( - state.curRenderedRowsCount + props.pageSize, - props.enableEmptySections - ? props.dataSource.getRowAndSectionCount() - : props.dataSource.getRowCount(), - ); - this._prevRenderedRowsCount = state.curRenderedRowsCount; - return { - curRenderedRowsCount: rowsToRender, - }; - }, - () => { - this._measureAndUpdateScrollProps(); - this._prevRenderedRowsCount = this.state.curRenderedRowsCount; - }, - ); - }, - - _getDistanceFromEnd: function(scrollProperties: Object) { - return ( - scrollProperties.contentLength - - scrollProperties.visibleLength - - scrollProperties.offset - ); - }, - - _updateVisibleRows: function(updatedFrames?: Array) { - if (!this.props.onChangeVisibleRows) { - return; // No need to compute visible rows if there is no callback - } - if (updatedFrames) { - updatedFrames.forEach(newFrame => { - this._childFrames[newFrame.index] = merge(newFrame); - }); - } - var isVertical = !this.props.horizontal; - var dataSource = this.props.dataSource; - var visibleMin = this.scrollProperties.offset; - var visibleMax = visibleMin + this.scrollProperties.visibleLength; - var allRowIDs = dataSource.rowIdentities; - - var header = this.props.renderHeader && this.props.renderHeader(); - var totalIndex = header ? 1 : 0; - var visibilityChanged = false; - var changedRows = {}; - for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) { - var rowIDs = allRowIDs[sectionIdx]; - if (rowIDs.length === 0) { - continue; - } - var sectionID = dataSource.sectionIdentities[sectionIdx]; - if (this.props.renderSectionHeader) { - totalIndex++; - } - var visibleSection = this._visibleRows[sectionID]; - if (!visibleSection) { - visibleSection = {}; - } - for (var rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) { - var rowID = rowIDs[rowIdx]; - var frame = this._childFrames[totalIndex]; - totalIndex++; - if ( - this.props.renderSeparator && - (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1) - ) { - totalIndex++; - } - if (!frame) { - break; - } - var rowVisible = visibleSection[rowID]; - var min = isVertical ? frame.y : frame.x; - var max = min + (isVertical ? frame.height : frame.width); - if ((!min && !max) || min === max) { - break; - } - if (min > visibleMax || max < visibleMin) { - if (rowVisible) { - visibilityChanged = true; - delete visibleSection[rowID]; - if (!changedRows[sectionID]) { - changedRows[sectionID] = {}; - } - changedRows[sectionID][rowID] = false; - } - } else if (!rowVisible) { - visibilityChanged = true; - visibleSection[rowID] = true; - if (!changedRows[sectionID]) { - changedRows[sectionID] = {}; - } - changedRows[sectionID][rowID] = true; - } - } - if (!isEmpty(visibleSection)) { - this._visibleRows[sectionID] = visibleSection; - } else if (this._visibleRows[sectionID]) { - delete this._visibleRows[sectionID]; - } - } - visibilityChanged && - this.props.onChangeVisibleRows(this._visibleRows, changedRows); - }, - - _onScroll: function(e: Object) { - var isVertical = !this.props.horizontal; - this.scrollProperties.visibleLength = - e.nativeEvent.layoutMeasurement[isVertical ? 'height' : 'width']; - this.scrollProperties.contentLength = - e.nativeEvent.contentSize[isVertical ? 'height' : 'width']; - this.scrollProperties.offset = - e.nativeEvent.contentOffset[isVertical ? 'y' : 'x']; - this._updateVisibleRows(e.nativeEvent.updatedChildFrames); - if (!this._maybeCallOnEndReached(e)) { - this._renderMoreRowsIfNeeded(); - } - - if ( - this.props.onEndReached && - this._getDistanceFromEnd(this.scrollProperties) > - this.props.onEndReachedThreshold - ) { - // Scrolled out of the end zone, so it should be able to trigger again. - this._sentEndForContentLength = null; - } - - this.props.onScroll && this.props.onScroll(e); - }, -}); - -export default ListView;