├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── components ├── CellWrapper.js ├── SectionHeader.js ├── SectionList.js └── SelectableSectionsListView.js ├── compositor.json ├── index.js └── 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 | # react-native-selectablesectionlistview 2 | 3 | A Listview with a sidebar to directly jump to sections. 4 | 5 | Please file issues for missing features or bugs. 6 | 7 | I apologize for the bad name. 8 | 9 | ![How it looks](http://lum.pe/sectionlistview.gif) 10 | 11 | ## Usage 12 | 13 | The most basic way to use this component is as follows: 14 | 15 | ```javascript 16 | var SelectableSectionsListView = require('react-native-selectablesectionlistview'); 17 | 18 | // inside your render function 19 | 25 | ``` 26 | 27 | You can find a more complete example below 28 | 29 | ## Props 30 | 31 | ### SelectableSectionsListView 32 | 33 | 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: 34 | 35 | * `onScroll` 36 | * `onScrollAnimationEnd` 37 | * `dataSource` 38 | * `renderRow` 39 | * `renderSectionHeader` 40 | 41 | #### data 42 | `array|object`, **required** 43 | The data to render in the listview 44 | 45 | #### hideSectionList 46 | `boolean` 47 | Whether to show the section listing or not. *Note: If the data your are providing to 48 | the component is an array, the section list will automatically be hidden.* 49 | 50 | #### getSectionTitle 51 | `function` 52 | Function to provide titles for the section headers 53 | 54 | #### getSectionListTitle 55 | `function` 56 | Function to provide titles for the section list items 57 | 58 | #### onCellSelect 59 | `function` 60 | Callback which should be called when a cell has been selected 61 | 62 | #### onScrollToSection 63 | `function` 64 | Callback which should be called when the user scrolls to a section 65 | 66 | #### cell 67 | `function` **required** 68 | The cell component to render for each row 69 | 70 | #### sectionListItem 71 | `function` 72 | A custom component to render for each section list item 73 | 74 | #### sectionHeader 75 | `function` 76 | A custom component to render for each section header 77 | 78 | #### footer 79 | `function` 80 | A custom component to render as footer 81 | **This props takes precedence over `renderFooter`** 82 | 83 | #### renderFooter 84 | `function` 85 | A custom function which has to return a valid React element, which will be 86 | used as footer. 87 | 88 | #### header 89 | `function` 90 | A custom component to render as header 91 | **This props takes precedence over `renderHeader`** 92 | 93 | #### renderHeader 94 | `function` 95 | A custom function which has to return a valid React element, which will be used as header. 96 | 97 | #### headerHeight 98 | `number` 99 | The height of the rendered header element. 100 | **Is required if a header element is used, so the positions can be calculated correctly** 101 | 102 | #### cellProps 103 | `object` 104 | An object containing additional props, which will be passed to each cell component 105 | 106 | #### sectionHeaderHeight 107 | `number` **required** 108 | The height of the section header component 109 | 110 | #### cellHeight 111 | `number` **required** 112 | The height of the cell component 113 | 114 | #### useDynamicHeights 115 | `boolean` 116 | 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` 117 | **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.** 118 | 119 | #### updateScrollState 120 | `boolean` 121 | Whether to set the current y offset as state and pass it to each cell during re-rendering 122 | 123 | #### style 124 | `object|number` 125 | Styles to pass to the container 126 | 127 | #### sectionListStyle 128 | `object|number` 129 | Styles to pass to the section list container 130 | 131 | --- 132 | ### Cell component 133 | 134 | 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. 135 | 136 | #### index 137 | `number` 138 | The index of the cell inside the current section 139 | 140 | #### sectionId 141 | `string` 142 | The id of the parent section 143 | 144 | #### isFirst 145 | `boolean` 146 | Whether the cell is the first in the section 147 | 148 | #### isLast 149 | `boolean` 150 | Whether the cell is the last in the section 151 | 152 | #### item 153 | `mixed` 154 | The item to render 155 | 156 | #### offsetY 157 | `number` 158 | The current y offset of the list view 159 | **If you do not specify `updateScrollState={true}` for the list component, this props will always be 0** 160 | 161 | #### onSelect 162 | `function` 163 | The function which should be called when a cell is being selected 164 | 165 | --- 166 | ### Section list item component 167 | 168 | These props are automatically passed to your component 169 | 170 | #### sectionId 171 | `string` 172 | The id of the parent section 173 | 174 | #### title 175 | `string` 176 | The title for this section. Either the return value of `getSectionListTitle` or the same value as `sectionId` 177 | 178 | ## Example 179 | 180 | ```javascript 181 | class SectionHeader extends Component { 182 | render() { 183 | // inline styles used for brevity, use a stylesheet when possible 184 | var textStyle = { 185 | textAlign:'center', 186 | color:'#fff', 187 | fontWeight:'700', 188 | fontSize:16 189 | }; 190 | 191 | var viewStyle = { 192 | backgroundColor: '#ccc' 193 | }; 194 | return ( 195 | 196 | {this.props.title} 197 | 198 | ); 199 | } 200 | } 201 | 202 | class SectionItem extends Component { 203 | render() { 204 | return ( 205 | {this.props.title} 206 | ); 207 | } 208 | } 209 | 210 | class Cell extends Component { 211 | render() { 212 | return ( 213 | 214 | {this.props.item} 215 | 216 | ); 217 | } 218 | } 219 | 220 | class MyComponent extends Component { 221 | 222 | constructor(props, context) { 223 | super(props, context); 224 | 225 | this.state = { 226 | data: { 227 | A: ['some','entries','are here'], 228 | B: ['some','entries','are here'], 229 | C: ['some','entries','are here'], 230 | D: ['some','entries','are here'], 231 | E: ['some','entries','are here'], 232 | F: ['some','entries','are here'], 233 | G: ['some','entries','are here'], 234 | H: ['some','entries','are here'], 235 | I: ['some','entries','are here'], 236 | J: ['some','entries','are here'], 237 | K: ['some','entries','are here'], 238 | L: ['some','entries','are here'], 239 | M: ['some','entries','are here'], 240 | N: ['some','entries','are here'], 241 | O: ['some','entries','are here'], 242 | P: ['some','entries','are here'], 243 | Q: ['some','entries','are here'], 244 | R: ['some','entries','are here'], 245 | S: ['some','entries','are here'], 246 | T: ['some','entries','are here'], 247 | U: ['some','entries','are here'], 248 | V: ['some','entries','are here'], 249 | X: ['some','entries','are here'], 250 | Y: ['some','entries','are here'], 251 | Z: ['some','entries','are here'], 252 | } 253 | }; 254 | } 255 | 256 | render() { 257 | return ( 258 | 266 | ); 267 | } 268 | } 269 | 270 | ``` 271 | -------------------------------------------------------------------------------- /components/CellWrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react-native'); 4 | var {Component, PropTypes, View} = React; 5 | 6 | class CellWrapper extends Component { 7 | 8 | componentDidMount() { 9 | this.props.updateTag && this.props.updateTag(this.refs.view.getNodeHandle(), this.props.sectionId); 10 | } 11 | 12 | render() { 13 | var Cell = this.props.component; 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | CellWrapper.propTypes = { 23 | 24 | /** 25 | * The id of the section 26 | */ 27 | sectionId: PropTypes.oneOfType([ 28 | PropTypes.number, 29 | PropTypes.string 30 | ]), 31 | 32 | /** 33 | * A component to render for each cell 34 | */ 35 | component: PropTypes.func.isRequired, 36 | 37 | /** 38 | * A function used to propagate the root nodes handle back to the parent 39 | */ 40 | updateTag: PropTypes.func 41 | 42 | }; 43 | 44 | 45 | module.exports = CellWrapper; 46 | -------------------------------------------------------------------------------- /components/SectionHeader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react-native'); 4 | var {Component, PropTypes, StyleSheet, View, Text} = React; 5 | var UIManager = require('NativeModules').UIManager; 6 | class SectionHeader extends Component { 7 | 8 | componentDidMount() { 9 | this.props.updateTag && this.props.updateTag(this.refs.view.getNodeHandle(), this.props.sectionId); 10 | } 11 | 12 | render() { 13 | var SectionComponent = this.props.component; 14 | var content = SectionComponent ? 15 | : 16 | {this.props.title}; 17 | 18 | return ( 19 | 20 | {content} 21 | 22 | ); 23 | } 24 | } 25 | 26 | var styles = StyleSheet.create({ 27 | container: { 28 | backgroundColor:'#f8f8f8', 29 | borderTopWidth: 1, 30 | borderTopColor: '#ececec' 31 | }, 32 | text: { 33 | fontWeight: '700', 34 | paddingTop:2, 35 | paddingBottom:2, 36 | paddingLeft: 2 37 | } 38 | }); 39 | 40 | SectionHeader.propTypes = { 41 | 42 | /** 43 | * The id of the section 44 | */ 45 | sectionId: PropTypes.oneOfType([ 46 | PropTypes.number, 47 | PropTypes.string 48 | ]), 49 | 50 | /** 51 | * A component to render for each section item 52 | */ 53 | component: PropTypes.func, 54 | 55 | /** 56 | * A function used to propagate the root nodes handle back to the parent 57 | */ 58 | updateTag: PropTypes.func 59 | 60 | }; 61 | 62 | module.exports = SectionHeader; 63 | -------------------------------------------------------------------------------- /components/SectionList.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react-native'); 4 | var {Component, PropTypes, StyleSheet, View, Text} = React; 5 | var UIManager = require('NativeModules').UIManager; 6 | 7 | var noop = () => {}; 8 | var returnTrue = () => true; 9 | 10 | class SectionList extends Component { 11 | 12 | constructor(props, context) { 13 | super(props, context); 14 | 15 | this.onSectionSelect = this.onSectionSelect.bind(this); 16 | this.resetSection = this.resetSection.bind(this); 17 | this.detectAndScrollToSection = this.detectAndScrollToSection.bind(this); 18 | this.lastSelectedIndex = null; 19 | } 20 | 21 | onSectionSelect(sectionId, fromTouch) { 22 | this.props.onSectionSelect && this.props.onSectionSelect(sectionId); 23 | 24 | if (!fromTouch) { 25 | this.lastSelectedIndex = null; 26 | } 27 | } 28 | 29 | resetSection() { 30 | this.lastSelectedIndex = null; 31 | } 32 | 33 | detectAndScrollToSection(e) { 34 | var ev = e.nativeEvent; 35 | var rect = {width:1, height:1, x: ev.locationX, y: ev.locationY}; 36 | 37 | UIManager.measureViewsInRect(rect, e.target, noop, (frames) => { 38 | if (frames.length) { 39 | var index = frames[0].index; 40 | if (this.lastSelectedIndex !== index) { 41 | this.lastSelectedIndex = index; 42 | this.onSectionSelect(this.props.sections[index], true); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | render() { 49 | var SectionComponent = this.props.component; 50 | var sections = this.props.sections.map((section, index) => { 51 | var title = this.props.getSectionListTitle ? 52 | this.props.getSectionListTitle(section) : 53 | section; 54 | 55 | var child = SectionComponent ? 56 | : 60 | 62 | {title} 63 | ; 64 | 65 | return ( 66 | 67 | {child} 68 | 69 | ); 70 | }); 71 | 72 | return ( 73 | 80 | {sections} 81 | 82 | ); 83 | } 84 | } 85 | 86 | SectionList.propTypes = { 87 | 88 | /** 89 | * A component to render for each section item 90 | */ 91 | component: PropTypes.func, 92 | 93 | /** 94 | * Function to provide a title the section list items. 95 | */ 96 | getSectionListTitle: PropTypes.func, 97 | 98 | /** 99 | * Function to be called upon selecting a section list item 100 | */ 101 | onSectionSelect: PropTypes.func, 102 | 103 | /** 104 | * The sections to render 105 | */ 106 | sections: PropTypes.array.isRequired, 107 | 108 | /** 109 | * A style to apply to the section list container 110 | */ 111 | style: PropTypes.oneOfType([ 112 | PropTypes.number, 113 | PropTypes.object, 114 | ]) 115 | }; 116 | 117 | var styles = StyleSheet.create({ 118 | container: { 119 | position: 'absolute', 120 | backgroundColor: 'transparent', 121 | alignItems:'center', 122 | justifyContent:'center', 123 | right: 0, 124 | top: 0, 125 | bottom: 0, 126 | width: 15 127 | }, 128 | 129 | item: { 130 | padding: 0 131 | }, 132 | 133 | text: { 134 | fontWeight: '700', 135 | color: '#008fff' 136 | } 137 | }); 138 | 139 | module.exports = SectionList; 140 | -------------------------------------------------------------------------------- /components/SelectableSectionsListView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* jshint esnext: true */ 3 | 4 | var React = require('react-native'); 5 | var {Component, ListView, StyleSheet, View, PropTypes} = React; 6 | var UIManager = require('NativeModules').UIManager; 7 | var merge = require('merge'); 8 | 9 | var SectionHeader = require('./SectionHeader'); 10 | var SectionList = require('./SectionList'); 11 | var CellWrapper = require('./CellWrapper'); 12 | 13 | class SelectableSectionsListView extends Component { 14 | 15 | constructor(props, context) { 16 | super(props, context); 17 | 18 | this.state = { 19 | dataSource: new ListView.DataSource({ 20 | rowHasChanged: (row1, row2) => row1 !== row2, 21 | sectionHeaderHasChanged: (prev, next) => prev !== next 22 | }), 23 | offsetY: 0 24 | }; 25 | 26 | this.renderFooter = this.renderFooter.bind(this); 27 | this.renderHeader = this.renderHeader.bind(this); 28 | this.renderRow = this.renderRow.bind(this); 29 | this.renderSectionHeader = this.renderSectionHeader.bind(this); 30 | 31 | this.onScroll = this.onScroll.bind(this); 32 | this.onScrollAnimationEnd = this.onScrollAnimationEnd.bind(this); 33 | this.scrollToSection = this.scrollToSection.bind(this); 34 | 35 | // used for dynamic scrolling 36 | // always the first cell of a section keyed by section id 37 | this.cellTagMap = {}; 38 | this.sectionTagMap = {}; 39 | this.updateTagInCellMap = this.updateTagInCellMap.bind(this); 40 | this.updateTagInSectionMap = this.updateTagInSectionMap.bind(this); 41 | } 42 | 43 | componentWillMount() { 44 | this.calculateTotalHeight(); 45 | } 46 | 47 | componentDidMount() { 48 | // push measuring into the next tick 49 | setTimeout(() => { 50 | UIManager.measure(this.refs.view.getNodeHandle(), (x,y,w,h) => { 51 | this.containerHeight = h; 52 | }); 53 | }, 0); 54 | } 55 | 56 | componentWillReceiveProps(nextProps) { 57 | if (nextProps.data && nextProps.data !== this.props.data) { 58 | this.calculateTotalHeight(nextProps.data); 59 | } 60 | } 61 | 62 | calculateTotalHeight(data) { 63 | data = data || this.props.data; 64 | 65 | if (Array.isArray(data)) { 66 | return; 67 | } 68 | 69 | this.sectionItemCount = {}; 70 | this.totalHeight = Object.keys(data) 71 | .reduce((carry, key) => { 72 | var itemCount = data[key].length; 73 | carry += itemCount * this.props.cellHeight; 74 | carry += this.props.sectionHeaderHeight; 75 | 76 | this.sectionItemCount[key] = itemCount; 77 | 78 | return carry; 79 | }, 0); 80 | } 81 | 82 | updateTagInSectionMap(tag, section) { 83 | this.sectionTagMap[section] = tag; 84 | } 85 | 86 | updateTagInCellMap(tag, section) { 87 | this.cellTagMap[section] = tag; 88 | } 89 | 90 | scrollToSection(section) { 91 | var y = 0; 92 | var headerHeight = this.props.headerHeight || 0; 93 | y += headerHeight; 94 | 95 | if (!this.props.useDynamicHeights) { 96 | var cellHeight = this.props.cellHeight; 97 | var sectionHeaderHeight = this.props.sectionHeaderHeight; 98 | var keys = Object.keys(this.props.data); 99 | var index = keys.indexOf(section); 100 | 101 | var numcells = 0; 102 | for (var i = 0; i < index; i++) { 103 | numcells += this.props.data[keys[i]].length; 104 | } 105 | 106 | sectionHeaderHeight = index * sectionHeaderHeight; 107 | y += numcells * cellHeight + sectionHeaderHeight; 108 | var maxY = this.totalHeight - this.containerHeight + headerHeight; 109 | y = y > maxY ? maxY : y; 110 | 111 | this.refs.listview.refs.listviewscroll.scrollTo(y, 0); 112 | } else { 113 | // this breaks, if not all of the listview is pre-rendered! 114 | UIManager.measure(this.cellTagMap[section], (x, y, w, h) => { 115 | y = y - this.props.sectionHeaderHeight; 116 | this.refs.listview.refs.listviewscroll.scrollTo(y, 0); 117 | }); 118 | } 119 | 120 | this.props.onScrollToSection && this.props.onScrollToSection(section); 121 | } 122 | 123 | renderSectionHeader(sectionData, sectionId) { 124 | var updateTag = this.props.useDynamicHeights ? 125 | this.updateTagInSectionMap : 126 | null; 127 | 128 | var title = this.props.getSectionTitle ? 129 | this.props.getSectionTitle(sectionId) : 130 | sectionId; 131 | 132 | return ( 133 | 140 | ); 141 | } 142 | 143 | renderFooter() { 144 | var Footer = this.props.footer; 145 | return