├── .gitignore
├── .jshintrc
├── LICENSE
├── README.md
├── components
├── CellWrapper.js
├── SectionHeader.js
├── SectionList.js
└── SelectableSectionsListView.js
├── index.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | workbench
3 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "-W093": true,
3 | "asi": false,
4 | "bitwise": true,
5 | "boss": false,
6 | "browser": false,
7 | "camelcase": true,
8 | "couch": false,
9 | "curly": true,
10 | "debug": false,
11 | "devel": true,
12 | "dojo": false,
13 | "eqeqeq": true,
14 | "eqnull": false,
15 | "esnext": true,
16 | "evil": false,
17 | "expr": true,
18 | "forin": false,
19 | "freeze": true,
20 | "funcscope": true,
21 | "gcl": false,
22 | "globalstrict": true,
23 | "immed": false,
24 | "indent": 2,
25 | "iterator": false,
26 | "jquery": false,
27 | "lastsemic": false,
28 | "latedef": false,
29 | "laxbreak": true,
30 | "laxcomma": false,
31 | "loopfunc": false,
32 | "maxcomplexity": false,
33 | "maxdepth": false,
34 | "maxerr": 50,
35 | "maxlen": 80,
36 | "maxparams": false,
37 | "maxstatements": false,
38 | "mootools": false,
39 | "moz": false,
40 | "multistr": false,
41 | "newcap": true,
42 | "noarg": true,
43 | "node": true,
44 | "noempty": true,
45 | "nonbsp": true,
46 | "nonew": true,
47 | "nonstandard": false,
48 | "notypeof": false,
49 | "noyield": false,
50 | "phantom": false,
51 | "plusplus": false,
52 | "predef": [
53 | "jasmine",
54 | "describe",
55 | "beforeEach",
56 | "it",
57 | "jest",
58 | "pit",
59 | "expect",
60 | "rootRequire"
61 | ],
62 | "proto": false,
63 | "prototypejs": false,
64 | "quotmark": true,
65 | "rhino": false,
66 | "scripturl": false,
67 | "shadow": false,
68 | "smarttabs": false,
69 | "strict": true,
70 | "sub": false,
71 | "supernew": false,
72 | "trailing": true,
73 | "undef": true,
74 | "unused": true,
75 | "validthis": false,
76 | "worker": false,
77 | "wsh": false,
78 | "yui": false
79 | }
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Johannes Lumpe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Based on johanneslumpe's react-native-selectablesectionlistview, thanks to him for the awesome component!
2 | 99% of this component was done by @johanneslumpe, and I just replaced the deprecated API for newer react native version(>=0.13), and used a little trick to support both iOS and Android platforms.
3 |
4 |
5 | You can find this component on npm:
6 | ```
7 | npm install react-native-alphabetlistview --save
8 | ```
9 |
10 | ## Changelog
11 |
12 | - **v2.0.0**
13 | + Support RN 0.25+.(Thanks [@mbernardeau](https://github.com/mbernardeau)). If you have problem with an earlier version of RN, try v0.1.3.
14 |
15 |
16 |
17 |
18 | # Following is the original readme
19 |
20 | A Listview with a sidebar to directly jump to sections.
21 |
22 | Please file issues for missing features or bugs.
23 |
24 | I apologize for the bad name.
25 |
26 | 
27 |
28 | ## Usage
29 |
30 | The most basic way to use this component is as follows:
31 |
32 | ```javascript
33 | var AlphabetListView = require('react-native-alphabetlistview');
34 |
35 | // inside your render function
36 |
42 | ```
43 |
44 | You can find a more complete example below
45 |
46 | ## Props
47 |
48 | ### SelectableSectionsListView
49 |
50 | All props are passed through to the underlying `ListView`, so you can specify all the available props for `ListView` normally - except the following, which are defined internally and will be overwritten:
51 |
52 | * `onScroll`
53 | * `onScrollAnimationEnd`
54 | * `dataSource`
55 | * `renderRow`
56 | * `renderSectionHeader`
57 |
58 | #### data
59 | `array|object`, **required**
60 | The data to render in the listview
61 |
62 | #### hideSectionList
63 | `boolean`
64 | Whether to show the section listing or not. *Note: If the data your are providing to
65 | the component is an array, the section list will automatically be hidden.*
66 |
67 | #### getSectionTitle
68 | `function`
69 | Function to provide titles for the section headers
70 |
71 | #### getSectionListTitle
72 | `function`
73 | Function to provide titles for the section list items
74 |
75 | #### onCellSelect
76 | `function`
77 | Callback which should be called when a cell has been selected
78 |
79 | #### onScrollToSection
80 | `function`
81 | Callback which should be called when the user scrolls to a section
82 |
83 | #### cell
84 | `function` **required**
85 | The cell component to render for each row
86 |
87 | #### sectionListItem
88 | `function`
89 | A custom component to render for each section list item
90 |
91 | #### sectionHeader
92 | `function`
93 | A custom component to render for each section header
94 |
95 | #### footer
96 | `function`
97 | A custom component to render as footer
98 | **This props takes precedence over `renderFooter`**
99 |
100 | #### renderFooter
101 | `function`
102 | A custom function which has to return a valid React element, which will be
103 | used as footer.
104 |
105 | #### header
106 | `function`
107 | A custom component to render as header
108 | **This props takes precedence over `renderHeader`**
109 |
110 | #### renderHeader
111 | `function`
112 | A custom function which has to return a valid React element, which will be used as header.
113 |
114 | #### headerHeight
115 | `number`
116 | The height of the rendered header element.
117 | **Is required if a header element is used, so the positions can be calculated correctly**
118 |
119 | #### cellProps
120 | `object`
121 | An object containing additional props, which will be passed to each cell component
122 |
123 | #### sectionHeaderHeight
124 | `number` **required**
125 | The height of the section header component
126 |
127 | #### cellHeight
128 | `number` **required**
129 | The height of the cell component
130 |
131 | #### useDynamicHeights
132 | `boolean`
133 | Whether to determine the y position to scroll to by calculating header and cell heights or by using the UIManager to measure the position of the destination element. Defaults to `false`
134 | **This is an experimental feature. For it to work properly you will most likely have to experiment with different values for `scrollRenderAheadDistance`, depending on the size of your data set.**
135 |
136 | #### updateScrollState
137 | `boolean`
138 | Whether to set the current y offset as state and pass it to each cell during re-rendering
139 |
140 | #### style
141 | `object|number`
142 | Styles to pass to the container
143 |
144 | #### sectionListStyle
145 | `object|number`
146 | Styles to pass to the section list container
147 |
148 | #### sectionListFontStyle
149 | `object|number`
150 | Styles to pass to the section list letters
151 |
152 | ---
153 | ### Cell component
154 |
155 | These props are automatically passed to your component. In addition to these, your cell will receive all props which you specified in the object you passed as `cellProps` prop to the listview.
156 |
157 | #### index
158 | `number`
159 | The index of the cell inside the current section
160 |
161 | #### sectionId
162 | `string`
163 | The id of the parent section
164 |
165 | #### isFirst
166 | `boolean`
167 | Whether the cell is the first in the section
168 |
169 | #### isLast
170 | `boolean`
171 | Whether the cell is the last in the section
172 |
173 | #### item
174 | `mixed`
175 | The item to render
176 |
177 | #### offsetY
178 | `number`
179 | The current y offset of the list view
180 | **If you do not specify `updateScrollState={true}` for the list component, this props will always be 0**
181 |
182 | #### onSelect
183 | `function`
184 | The function which should be called when a cell is being selected
185 |
186 | ---
187 | ### Section list item component
188 |
189 | These props are automatically passed to your component
190 |
191 | #### sectionId
192 | `string`
193 | The id of the parent section
194 |
195 | #### title
196 | `string`
197 | The title for this section. Either the return value of `getSectionListTitle` or the same value as `sectionId`
198 |
199 | ## Example
200 |
201 | ```javascript
202 | class SectionHeader extends Component {
203 | render() {
204 | // inline styles used for brevity, use a stylesheet when possible
205 | var textStyle = {
206 | textAlign:'center',
207 | color:'#fff',
208 | fontWeight:'700',
209 | fontSize:16
210 | };
211 |
212 | var viewStyle = {
213 | backgroundColor: '#ccc'
214 | };
215 | return (
216 |
217 | {this.props.title}
218 |
219 | );
220 | }
221 | }
222 |
223 | class SectionItem extends Component {
224 | render() {
225 | return (
226 | {this.props.title}
227 | );
228 | }
229 | }
230 |
231 | class Cell extends Component {
232 | render() {
233 | return (
234 |
235 | {this.props.item}
236 |
237 | );
238 | }
239 | }
240 |
241 | class MyComponent extends Component {
242 |
243 | constructor(props, context) {
244 | super(props, context);
245 |
246 | this.state = {
247 | data: {
248 | A: ['some','entries','are here'],
249 | B: ['some','entries','are here'],
250 | C: ['some','entries','are here'],
251 | D: ['some','entries','are here'],
252 | E: ['some','entries','are here'],
253 | F: ['some','entries','are here'],
254 | G: ['some','entries','are here'],
255 | H: ['some','entries','are here'],
256 | I: ['some','entries','are here'],
257 | J: ['some','entries','are here'],
258 | K: ['some','entries','are here'],
259 | L: ['some','entries','are here'],
260 | M: ['some','entries','are here'],
261 | N: ['some','entries','are here'],
262 | O: ['some','entries','are here'],
263 | P: ['some','entries','are here'],
264 | Q: ['some','entries','are here'],
265 | R: ['some','entries','are here'],
266 | S: ['some','entries','are here'],
267 | T: ['some','entries','are here'],
268 | U: ['some','entries','are here'],
269 | V: ['some','entries','are here'],
270 | W: ['some','entries','are here'],
271 | X: ['some','entries','are here'],
272 | Y: ['some','entries','are here'],
273 | Z: ['some','entries','are here'],
274 | }
275 | };
276 | }
277 |
278 | render() {
279 | return (
280 |
288 | );
289 | }
290 | }
291 |
292 | ```
293 |
--------------------------------------------------------------------------------
/components/CellWrapper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { Component } from 'react';
4 | import PropTypes from 'prop-types';
5 | import ReactNative, {
6 | View
7 | } from 'react-native';
8 |
9 | export default class CellWrapper extends Component {
10 | componentDidMount() {
11 | this.props.updateTag && this.props.updateTag(ReactNative.findNodeHandle(this.refs.view), this.props.sectionId);
12 | }
13 |
14 | render() {
15 | const Cell = this.props.component;
16 | return (
17 |
18 | |
19 |
20 | );
21 | }
22 | }
23 |
24 | CellWrapper.propTypes = {
25 |
26 | /**
27 | * The id of the section
28 | */
29 | sectionId: PropTypes.oneOfType([
30 | PropTypes.number,
31 | PropTypes.string
32 | ]),
33 |
34 | /**
35 | * A component to render for each cell
36 | */
37 | component: PropTypes.func.isRequired,
38 |
39 | /**
40 | * A function used to propagate the root nodes handle back to the parent
41 | */
42 | updateTag: PropTypes.func
43 |
44 | };
45 |
--------------------------------------------------------------------------------
/components/SectionHeader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { Component } from 'react';
4 | import PropTypes from 'prop-types';
5 | import ReactNative, {
6 | StyleSheet,
7 | View,
8 | Text,
9 | NativeModules
10 | } from 'react-native';
11 |
12 | const { UIManager } = NativeModules;
13 |
14 | export default class SectionHeader extends Component {
15 |
16 | componentDidMount() {
17 | this.props.updateTag && this.props.updateTag(ReactNative.findNodeHandle(this.refs.view), this.props.sectionId);
18 | }
19 |
20 | render() {
21 | const SectionComponent = this.props.component;
22 | const content = SectionComponent ?
23 | :
24 | {this.props.title};
25 |
26 | return (
27 |
28 | {content}
29 |
30 | );
31 | }
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | container: {
36 | backgroundColor: '#f8f8f8',
37 | borderTopWidth: 1,
38 | borderTopColor: '#ececec'
39 | },
40 | text: {
41 | fontWeight: '700',
42 | paddingTop: 2,
43 | paddingBottom: 2,
44 | paddingLeft: 2
45 | }
46 | });
47 |
48 | SectionHeader.propTypes = {
49 |
50 | /**
51 | * The id of the section
52 | */
53 | sectionId: PropTypes.oneOfType([
54 | PropTypes.number,
55 | PropTypes.string
56 | ]),
57 |
58 | /**
59 | * A component to render for each section item
60 | */
61 | component: PropTypes.func,
62 |
63 | /**
64 | * A function used to propagate the root nodes handle back to the parent
65 | */
66 | updateTag: PropTypes.func
67 | };
68 |
--------------------------------------------------------------------------------
/components/SectionList.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { Component } from 'react';
4 | import PropTypes from 'prop-types';
5 | import ReactNative, {
6 | StyleSheet,
7 | View,
8 | Text,
9 | NativeModules,
10 | } from 'react-native';
11 |
12 | const { UIManager } = NativeModules;
13 |
14 | const noop = () => {};
15 | const returnTrue = () => true;
16 |
17 | export default class SectionList extends Component {
18 |
19 | constructor(props, context) {
20 | super(props, context);
21 |
22 | this.onSectionSelect = this.onSectionSelect.bind(this);
23 | this.resetSection = this.resetSection.bind(this);
24 | this.detectAndScrollToSection = this.detectAndScrollToSection.bind(this);
25 | this.lastSelectedIndex = null;
26 | }
27 |
28 | onSectionSelect(sectionId, fromTouch) {
29 | this.props.onSectionSelect && this.props.onSectionSelect(sectionId);
30 |
31 | if (!fromTouch) {
32 | this.lastSelectedIndex = null;
33 | }
34 | }
35 |
36 | resetSection() {
37 | this.lastSelectedIndex = null;
38 | }
39 |
40 | detectAndScrollToSection(e) {
41 | const ev = e.nativeEvent.touches[0];
42 | //var rect = {width:1, height:1, x: ev.locationX, y: ev.locationY};
43 | //var rect = [ev.locationX, ev.locationY];
44 |
45 | //UIManager.measureViewsInRect(rect, e.target, noop, (frames) => {
46 | // if (frames.length) {
47 | // var index = frames[0].index;
48 | // if (this.lastSelectedIndex !== index) {
49 | // this.lastSelectedIndex = index;
50 | // this.onSectionSelect(this.props.sections[index], true);
51 | // }
52 | // }
53 | //});
54 | //UIManager.findSubviewIn(e.target, rect, viewTag => {
55 | //this.onSectionSelect(view, true);
56 | //})
57 | const targetY = ev.pageY;
58 | const { y, width, height } = this.measure;
59 | const index = (Math.floor(ev.locationY / height));
60 | if (index >= this.props.sections.length || index < 0) {
61 | return;
62 | }
63 |
64 | if (this.lastSelectedIndex !== index && this.props.data[this.props.sections[index]].length) {
65 | this.lastSelectedIndex = index;
66 | this.onSectionSelect(this.props.sections[index], true);
67 | }
68 | }
69 |
70 | fixSectionItemMeasure() {
71 | const sectionItem = this.refs.sectionItem0;
72 | if (!sectionItem) {
73 | return;
74 | }
75 | this.measureTimer = setTimeout(() => {
76 | sectionItem.measure((x, y, width, height, pageX, pageY) => {
77 | //console.log([x, y, width, height, pageX, pageY]);
78 | this.measure = {
79 | y: pageY,
80 | width,
81 | height
82 | };
83 | })
84 | }, 0);
85 | }
86 |
87 | componentDidMount() {
88 | this.fixSectionItemMeasure();
89 | }
90 |
91 | // fix bug when change data
92 | componentDidUpdate() {
93 | this.fixSectionItemMeasure();
94 | }
95 |
96 | componentWillUnmount() {
97 | this.measureTimer && clearTimeout(this.measureTimer);
98 | }
99 |
100 | render() {
101 | const SectionComponent = this.props.component;
102 | const sections = this.props.sections.map((section, index) => {
103 | const title = this.props.getSectionListTitle ?
104 | this.props.getSectionListTitle(section) :
105 | section;
106 |
107 | const textStyle = this.props.data[section].length ?
108 | styles.text :
109 | styles.inactivetext;
110 |
111 | const child = SectionComponent ?
112 | :
116 |
118 | {title}
119 | ;
120 |
121 | //if(index){
122 | return (
123 |
124 | {child}
125 |
126 | );
127 | //}
128 | //else{
129 | // return (
130 | // {console.log(e.nativeEvent.layout)}}>
132 | // {child}
133 | //
134 | // );
135 | //
136 | //}
137 | });
138 |
139 | return (
140 |
147 | {sections}
148 |
149 | );
150 | }
151 | }
152 |
153 | SectionList.propTypes = {
154 |
155 | /**
156 | * A component to render for each section item
157 | */
158 | component: PropTypes.func,
159 |
160 | /**
161 | * Function to provide a title the section list items.
162 | */
163 | getSectionListTitle: PropTypes.func,
164 |
165 | /**
166 | * Function to be called upon selecting a section list item
167 | */
168 | onSectionSelect: PropTypes.func,
169 |
170 | /**
171 | * The sections to render
172 | */
173 | sections: PropTypes.array.isRequired,
174 |
175 | /**
176 | * A style to apply to the section list container
177 | */
178 | style: PropTypes.oneOfType([
179 | PropTypes.number,
180 | PropTypes.object,
181 | ]),
182 |
183 | /**
184 | * Text font size
185 | */
186 | fontStyle: PropTypes.oneOfType([
187 | PropTypes.number,
188 | PropTypes.object,
189 | ]),
190 | };
191 |
192 | const styles = StyleSheet.create({
193 | container: {
194 | position: 'absolute',
195 | backgroundColor: 'transparent',
196 | alignItems:'flex-end',
197 | justifyContent:'flex-start',
198 | right: 5,
199 | top: 0,
200 | bottom: 0
201 | },
202 |
203 | item: {
204 | padding: 0
205 | },
206 |
207 | text: {
208 | fontWeight: '700',
209 | color: '#008fff'
210 | },
211 |
212 | inactivetext: {
213 | fontWeight: '700',
214 | color: '#CCCCCC'
215 | }
216 | });
217 |
--------------------------------------------------------------------------------
/components/SelectableSectionsListView.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* jshint esnext: true */
3 |
4 | import React, { Component } from 'react';
5 | import PropTypes from 'prop-types';
6 | import ReactNative, {
7 | ListView,
8 | StyleSheet,
9 | View,
10 | NativeModules,
11 | } from 'react-native';
12 | import merge from 'merge';
13 |
14 | import SectionHeader from './SectionHeader';
15 | import SectionList from './SectionList';
16 | import CellWrapper from './CellWrapper';
17 |
18 | const { UIManager } = NativeModules;
19 |
20 | export default class SelectableSectionsListView extends Component {
21 |
22 | constructor(props, context) {
23 | super(props, context);
24 |
25 | this.state = {
26 | dataSource: new ListView.DataSource({
27 | rowHasChanged: (row1, row2) => row1 !== row2,
28 | sectionHeaderHasChanged: (prev, next) => prev !== next
29 | }),
30 | offsetY: 0
31 | };
32 |
33 | this.renderFooter = this.renderFooter.bind(this);
34 | this.renderHeader = this.renderHeader.bind(this);
35 | this.renderRow = this.renderRow.bind(this);
36 | this.renderSectionHeader = this.renderSectionHeader.bind(this);
37 |
38 | this.onScroll = this.onScroll.bind(this);
39 | this.onScrollAnimationEnd = this.onScrollAnimationEnd.bind(this);
40 | this.scrollToSection = this.scrollToSection.bind(this);
41 |
42 | // used for dynamic scrolling
43 | // always the first cell of a section keyed by section id
44 | this.cellTagMap = {};
45 | this.sectionTagMap = {};
46 | this.updateTagInCellMap = this.updateTagInCellMap.bind(this);
47 | this.updateTagInSectionMap = this.updateTagInSectionMap.bind(this);
48 | }
49 |
50 | componentWillMount() {
51 | this.calculateTotalHeight();
52 | }
53 |
54 | componentDidMount() {
55 | // push measuring into the next tick
56 | setTimeout(() => {
57 | UIManager.measure(ReactNative.findNodeHandle(this.refs.view), (x,y,w,h) => {
58 | this.containerHeight = h;
59 | if (this.props.contentInset && this.props.data && this.props.data.length > 0) {
60 | this.scrollToSection(Object.keys(this.props.data)[0]);
61 | }
62 | });
63 | }, 0);
64 | }
65 |
66 | componentWillReceiveProps(nextProps) {
67 | if (nextProps.data && nextProps.data !== this.props.data) {
68 | this.calculateTotalHeight(nextProps.data);
69 | }
70 | }
71 |
72 | calculateTotalHeight(data) {
73 | data = data || this.props.data;
74 |
75 | if (Array.isArray(data)) {
76 | return;
77 | }
78 |
79 | this.sectionItemCount = {};
80 | this.totalHeight = Object.keys(data)
81 | .reduce((carry, key) => {
82 | var itemCount = data[key].length;
83 | carry += itemCount * this.props.cellHeight;
84 | carry += this.props.sectionHeaderHeight;
85 |
86 | this.sectionItemCount[key] = itemCount;
87 |
88 | return carry;
89 | }, 0);
90 | }
91 |
92 | updateTagInSectionMap(tag, section) {
93 | this.sectionTagMap[section] = tag;
94 | }
95 |
96 | updateTagInCellMap(tag, section) {
97 | this.cellTagMap[section] = tag;
98 | }
99 |
100 | scrollToSection(section) {
101 | let y = 0;
102 | let headerHeight = this.props.headerHeight || 0;
103 | y += headerHeight;
104 |
105 | if(this.props.contentInset) {
106 | y -= this.props.contentInset.top - headerHeight
107 | }
108 |
109 | if (!this.props.useDynamicHeights) {
110 | const cellHeight = this.props.cellHeight;
111 | let sectionHeaderHeight = this.props.sectionHeaderHeight;
112 | let keys = Object.keys(this.props.data);
113 | if (typeof(this.props.compareFunction) === "function") {
114 | keys = keys.sort(this.props.compareFunction);
115 | }
116 | const index = keys.indexOf(section);
117 |
118 | let numcells = 0;
119 | for (var i = 0; i < index; i++) {
120 | numcells += this.props.data[keys[i]].length;
121 | }
122 |
123 | sectionHeaderHeight = index * sectionHeaderHeight;
124 | y += numcells * cellHeight + sectionHeaderHeight;
125 | const maxY = this.totalHeight - this.containerHeight + headerHeight;
126 | y = y > maxY ? maxY : y;
127 |
128 | this.refs.listview.scrollTo({ x:0, y, animated: true });
129 | } else {
130 | UIManager.measureLayout(this.cellTagMap[section], ReactNative.findNodeHandle(this.refs.listview), () => {}, (x, y, w, h) => {
131 | y = y - this.props.sectionHeaderHeight;
132 | this.refs.listview.scrollTo({ x:0, y, animated: true });
133 | });
134 | }
135 |
136 | this.props.onScrollToSection && this.props.onScrollToSection(section);
137 | }
138 |
139 | renderSectionHeader(sectionData, sectionId) {
140 | const updateTag = this.props.useDynamicHeights ?
141 | this.updateTagInSectionMap :
142 | null;
143 |
144 | const title = this.props.getSectionTitle ?
145 | this.props.getSectionTitle(sectionId) :
146 | sectionId;
147 |
148 | return (
149 |
156 | );
157 | }
158 |
159 | renderFooter() {
160 | const Footer = this.props.footer;
161 | return ;
162 | }
163 |
164 | renderHeader() {
165 | const Header = this.props.header;
166 | return ;
167 | }
168 |
169 | renderRow(item, sectionId, index) {
170 | const CellComponent = this.props.cell;
171 | index = parseInt(index, 10);
172 |
173 | const isFirst = index === 0;
174 | const isLast = this.sectionItemCount && this.sectionItemCount[sectionId]-1 === index;
175 |
176 | const props = {
177 | isFirst,
178 | isLast,
179 | sectionId,
180 | index,
181 | item,
182 | offsetY: this.state.offsetY,
183 | onSelect: this.props.onCellSelect
184 | };
185 |
186 | return index === 0 && this.props.useDynamicHeights ?
187 | :
190 | ;
191 | }
192 |
193 | onScroll(e) {
194 | const offsetY = e.nativeEvent.contentOffset.y;
195 | if (this.props.updateScrollState) {
196 | this.setState({
197 | offsetY
198 | });
199 | }
200 |
201 | this.props.onScroll && this.props.onScroll(e);
202 | }
203 |
204 | onScrollAnimationEnd(e) {
205 | if (this.props.updateScrollState) {
206 | this.setState({
207 | offsetY: e.nativeEvent.contentOffset.y
208 | });
209 | }
210 | }
211 |
212 | render() {
213 | const { data } = this.props;
214 | const dataIsArray = Array.isArray(data);
215 | let sectionList;
216 | let renderSectionHeader;
217 | let dataSource;
218 | let sections = Object.keys(data);
219 |
220 | if (typeof(this.props.compareFunction) === "function") {
221 | sections = sections.sort(this.props.compareFunction);
222 | }
223 |
224 | if (dataIsArray) {
225 | dataSource = this.state.dataSource.cloneWithRows(data);
226 | } else {
227 | sectionList = !this.props.hideSectionList ?
228 | :
237 | null;
238 |
239 | renderSectionHeader = this.renderSectionHeader;
240 | dataSource = this.state.dataSource.cloneWithRowsAndSections(data, sections);
241 | }
242 |
243 | const renderFooter = this.props.footer ?
244 | this.renderFooter :
245 | this.props.renderFooter;
246 |
247 | const renderHeader = this.props.header ?
248 | this.renderHeader :
249 | this.props.renderHeader;
250 |
251 | const props = merge({}, this.props, {
252 | onScroll: this.onScroll,
253 | onScrollAnimationEnd: this.onScrollAnimationEnd,
254 | dataSource,
255 | renderFooter,
256 | renderHeader,
257 | renderRow: this.renderRow,
258 | renderSectionHeader
259 | });
260 |
261 | props.style = void 0;
262 |
263 | return (
264 |
265 |
269 | {sectionList}
270 |
271 | );
272 | }
273 | }
274 |
275 | const styles = StyleSheet.create({
276 | container: {
277 | flex: 1
278 | }
279 | });
280 |
281 | const stylesheetProp = PropTypes.oneOfType([
282 | PropTypes.number,
283 | PropTypes.object,
284 | ]);
285 |
286 | SelectableSectionsListView.propTypes = {
287 | /**
288 | * The data to render in the listview
289 | */
290 | data: PropTypes.oneOfType([
291 | PropTypes.array,
292 | PropTypes.object,
293 | ]).isRequired,
294 |
295 | /**
296 | * Whether to show the section listing or not
297 | */
298 | hideSectionList: PropTypes.bool,
299 |
300 | /**
301 | * Functions to provide a title for the section header and the section list
302 | * items. If not provided, the section ids will be used (the keys from the data object)
303 | */
304 | getSectionTitle: PropTypes.func,
305 | getSectionListTitle: PropTypes.func,
306 |
307 | /**
308 | * Function to sort sections. If not provided, the sections order will match data source
309 | */
310 | compareFunction: PropTypes.func,
311 |
312 | /**
313 | * Callback which should be called when a cell has been selected
314 | */
315 | onCellSelect: PropTypes.func,
316 |
317 | /**
318 | * Callback which should be called when the user scrolls to a section
319 | */
320 | onScrollToSection: PropTypes.func,
321 |
322 | /**
323 | * The cell element to render for each row
324 | */
325 | cell: PropTypes.func.isRequired,
326 |
327 | /**
328 | * A custom element to render for each section list item
329 | */
330 | sectionListItem: PropTypes.func,
331 |
332 | /**
333 | * A custom element to render for each section header
334 | */
335 | sectionHeader: PropTypes.func,
336 |
337 | /**
338 | * A custom element to render as footer
339 | */
340 | footer: PropTypes.func,
341 |
342 | /**
343 | * A custom element to render as header
344 | */
345 | header: PropTypes.func,
346 |
347 | /**
348 | * The height of the header element to render. Is required if a
349 | * header element is used, so the positions can be calculated correctly
350 | */
351 | headerHeight: PropTypes.number,
352 |
353 | /**
354 | * A custom function to render as footer
355 | */
356 | renderHeader: PropTypes.func,
357 |
358 | /**
359 | * A custom function to render as header
360 | */
361 | renderFooter: PropTypes.func,
362 |
363 | /**
364 | * An object containing additional props, which will be passed
365 | * to each cell component
366 | */
367 | cellProps: PropTypes.object,
368 |
369 | /**
370 | * The height of the section header component
371 | */
372 | sectionHeaderHeight: PropTypes.number.isRequired,
373 |
374 | /**
375 | * The height of the cell component
376 | */
377 | cellHeight: PropTypes.number.isRequired,
378 |
379 | /**
380 | * Whether to determine the y postion to scroll to by calculating header and
381 | * cell heights or by using the UIManager to measure the position of the
382 | * destination element. This is an exterimental feature
383 | */
384 | useDynamicHeights: PropTypes.bool,
385 |
386 | /**
387 | * Whether to set the current y offset as state and pass it to each
388 | * cell during re-rendering
389 | */
390 | updateScrollState: PropTypes.bool,
391 |
392 | /**
393 | * Styles to pass to the container
394 | */
395 | style: stylesheetProp,
396 |
397 | /**
398 | * Styles to pass to the section list container
399 | */
400 | sectionListStyle: stylesheetProp,
401 |
402 | /**
403 | * Selector styles
404 | */
405 | sectionListFontStyle: stylesheetProp,
406 | };
407 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import SelectableSectionsListView from './components/SelectableSectionsListView';
4 |
5 | export default SelectableSectionsListView;
6 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-alphabetlistview",
3 | "version": "0.3.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "asap": {
8 | "version": "2.0.6",
9 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
10 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
11 | },
12 | "core-js": {
13 | "version": "1.2.7",
14 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
15 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
16 | },
17 | "encoding": {
18 | "version": "0.1.12",
19 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
20 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
21 | "requires": {
22 | "iconv-lite": "0.4.19"
23 | }
24 | },
25 | "fbjs": {
26 | "version": "0.8.16",
27 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
28 | "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=",
29 | "requires": {
30 | "core-js": "1.2.7",
31 | "isomorphic-fetch": "2.2.1",
32 | "loose-envify": "1.3.1",
33 | "object-assign": "4.1.1",
34 | "promise": "7.3.1",
35 | "setimmediate": "1.0.5",
36 | "ua-parser-js": "0.7.17"
37 | }
38 | },
39 | "iconv-lite": {
40 | "version": "0.4.19",
41 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
42 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
43 | },
44 | "is-stream": {
45 | "version": "1.1.0",
46 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
47 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
48 | },
49 | "isomorphic-fetch": {
50 | "version": "2.2.1",
51 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
52 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
53 | "requires": {
54 | "node-fetch": "1.7.3",
55 | "whatwg-fetch": "2.0.3"
56 | }
57 | },
58 | "js-tokens": {
59 | "version": "3.0.2",
60 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
61 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
62 | },
63 | "loose-envify": {
64 | "version": "1.3.1",
65 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
66 | "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
67 | "requires": {
68 | "js-tokens": "3.0.2"
69 | }
70 | },
71 | "node-fetch": {
72 | "version": "1.7.3",
73 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
74 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
75 | "requires": {
76 | "encoding": "0.1.12",
77 | "is-stream": "1.1.0"
78 | }
79 | },
80 | "object-assign": {
81 | "version": "4.1.1",
82 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
83 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
84 | },
85 | "promise": {
86 | "version": "7.3.1",
87 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
88 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
89 | "requires": {
90 | "asap": "2.0.6"
91 | }
92 | },
93 | "prop-types": {
94 | "version": "15.6.0",
95 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz",
96 | "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=",
97 | "requires": {
98 | "fbjs": "0.8.16",
99 | "loose-envify": "1.3.1",
100 | "object-assign": "4.1.1"
101 | }
102 | },
103 | "setimmediate": {
104 | "version": "1.0.5",
105 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
106 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
107 | },
108 | "ua-parser-js": {
109 | "version": "0.7.17",
110 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
111 | "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g=="
112 | },
113 | "whatwg-fetch": {
114 | "version": "2.0.3",
115 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz",
116 | "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ="
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-alphabetlistview",
3 | "version": "0.3.0",
4 | "description": "A Listview with a sidebar to jump to sections directly, based on johanneslumpe's react-native-selectablesectionlistview",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/sunnylqm/react-native-alphabetlistview.git"
12 | },
13 | "author": "Johannes Lumpe (https://github.com/johanneslumpe), sunnylqm (https://github.com/sunnylqm), Kenton Jacobsen (https://github.com/brokentone)",
14 | "license": "MIT",
15 | "keywords": [
16 | "react-component",
17 | "react-native",
18 | "ios",
19 | "android"
20 | ],
21 | "dependencies": {
22 | "merge": "^1.2.0",
23 | "prop-types": "^15.6.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------