├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── example ├── ExampleData.jsx ├── ExampleTable.jsx ├── GriddleWithCallback.jsx ├── demo │ ├── css │ │ ├── griddle.css │ │ └── style.css │ └── index.html ├── main.js └── taffy-min.js ├── gulpfile.js ├── package.json └── src ├── main.jsx ├── react-datepicker ├── calendar.js ├── date_input.js ├── datepicker.js ├── day.js ├── popover.js └── util │ └── date.js └── react-typeahead ├── keyevent.js ├── react-typeahead.js ├── tokenizer ├── index.js └── token.js └── typeahead ├── index.js ├── option.js └── selector.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /example/demo/main.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For react-structured-filter software 4 | 5 | Copyright (c) 2015, Summit Route LLC. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name Facebook nor the names of its contributors may be used to 19 | endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | react-structured-filter library 2 | Copyright (c) 2015, Summit Route LLC. 3 | For license information see the LICENSE file which accompanies this 4 | NOTICE file. 5 | 6 | 7 | 8 | This product contains a modified version of the react-typeahead library which is Copyright (c) 2013, Peter Ruibal. 9 | 10 | * License: ISC 11 | * https://github.com/fmoo/react-typeahead/blob/master/LICENSE 12 | * Homepage: 13 | * https://github.com/fmoo/react-typeahead 14 | 15 | 16 | 17 | This product contains a modified version of the react-datepicker library which is Copyright (c) 2014 HackerOne Inc and individual contributers. 18 | 19 | * License: MIT 20 | * https://github.com/Hacker0x01/react-datepicker/blob/master/LICENSE 21 | * Homepage: 22 | * https://github.com/Hacker0x01/react-datepicker -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-structured-filter (unmaintained) 3 | react-structured-filter is a javascript library that provides autocomplete faceted search queries. 4 | This was inspired by [visualsearch](http://documentcloud.github.io/visualsearch) and 5 | [structured-filter](https://github.com/evoluteur/structured-filter) but redone for 6 | [React](http://facebook.github.io/react/). 7 | 8 | It is heavily based on [react-typeahead](https://github.com/fmoo/react-typeahead) and uses some modified code from 9 | [react-datepicker](https://github.com/Hacker0x01/react-datepicker). 10 | It was developed to be used with [Griddle](http://dynamictyped.github.io/Griddle/), 11 | but should be usable with [fixed-data-table](https://github.com/facebook/fixed-data-table). 12 | 13 | It is used by [Summit Route](https://summitroute.com/) internally for analyzing our data. 14 | We needed an interface to provide advanced querying capabilities. 15 | Be aware that it might be confusing to your users and queries can be constructed that may not be performant on your dataset. 16 | 17 | The demo provided uses static data sent down to the client. 18 | You should poll data from a server and do filtering on a real database. 19 | 20 | ## Demo 21 | Check out the [docs](http://summitroute.github.io/react-structured-filter/) and [demo](http://summitroute.github.io/react-structured-filter/demo.html) 22 | 23 | ### License 24 | BSD License 25 | -------------------------------------------------------------------------------- /example/ExampleTable.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Griddle = require('griddle-react'); 3 | var GriddleWithCallback = require('./GriddleWithCallback.jsx'); 4 | var StructuredFilter = require('../src/main.jsx'); 5 | 6 | var ExampleData = require('./ExampleData.jsx'); 7 | 8 | var ExampleTable = React.createClass({ 9 | getInitialState: function() { 10 | return { 11 | filter: "", 12 | } 13 | }, 14 | 15 | 16 | getJsonData: function(filterString, sortColumn, sortAscending, page, pageSize, callback) { 17 | thisComponent = this; 18 | 19 | if (filterString==undefined) { 20 | filterString = ""; 21 | } 22 | if (sortColumn==undefined) { 23 | sortColumn = ""; 24 | } 25 | 26 | // Normally you would make a Reqwest here to the server 27 | var results = this.refs.ExampleData.filter(filterString, sortColumn, sortAscending, page, pageSize); 28 | callback(results); 29 | }, 30 | 31 | 32 | updateFilter: function(filter){ 33 | // Set our filter to json data of the current filter tokens 34 | this.setState({filter: JSON.stringify(filter)}); 35 | }, 36 | 37 | 38 | getSymbolOptions: function() { 39 | return this.refs.ExampleData.getSymbolOptions(); 40 | }, 41 | 42 | getSectorOptions: function() { 43 | return this.refs.ExampleData.getSectorOptions(); 44 | }, 45 | 46 | getIndustryOptions: function() { 47 | return this.refs.ExampleData.getIndustryOptions(); 48 | }, 49 | 50 | 51 | render: function(){ 52 | return ( 53 |
54 | 73 | 77 | 78 |
79 | ) 80 | } 81 | }); 82 | module.exports = ExampleTable; 83 | -------------------------------------------------------------------------------- /example/GriddleWithCallback.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var _ = require('underscore'); 3 | var Griddle = require('griddle-react'); 4 | 5 | var Loading = React.createClass({ 6 | getDefaultProps: function(){ 7 | return { 8 | loadingText: "Loading" 9 | } 10 | }, 11 | render: function(){ 12 | return
{this.props.loadingText}
; 13 | } 14 | }); 15 | 16 | var NextArrow = React.createElement("i", {className: "glyphicon glyphicon-chevron-right"}, null); 17 | var PreviousArrow = React.createElement("i", {className: "glyphicon glyphicon-chevron-left"}, null); 18 | var SettingsIconComponent = React.createElement("i", {className: "glyphicon glyphicon-cog"}, null); 19 | 20 | var GriddleWithCallback = React.createClass({ 21 | /** 22 | * 23 | */ 24 | getDefaultProps: function(){ 25 | return { 26 | getExternalResults: null, 27 | resultsPerPage: 10, 28 | loadingComponent: null, 29 | enableInfiniteScroll: false, 30 | filter: "" 31 | } 32 | }, 33 | 34 | 35 | /** 36 | * 37 | */ 38 | getInitialState: function(){ 39 | var initial = { "results": [], 40 | "page": 0, 41 | "maxPage": 0, 42 | "sortColumn":null, 43 | "sortAscending":true 44 | }; 45 | 46 | // If we need to get external results, grab the results. 47 | initial.isLoading = true; // Initialize to 'loading' 48 | 49 | return initial; 50 | }, 51 | 52 | 53 | /** 54 | * Called when component mounts 55 | */ 56 | componentDidMount: function(){ 57 | var state = this.state; 58 | state.pageSize = this.props.resultsPerPage; 59 | 60 | var that = this; 61 | 62 | if (!this.hasExternalResults()) { 63 | console.error("When using GriddleWithCallback, a getExternalResults callback must be supplied."); 64 | return; 65 | } 66 | 67 | // Update the state with external results when mounting 68 | state = this.updateStateWithExternalResults(state, function(updatedState) { 69 | that.setState(updatedState); 70 | }); 71 | }, 72 | 73 | 74 | /** 75 | * 76 | */ 77 | componentWillReceiveProps: function(nextProps) { 78 | var state = this.state, 79 | that = this; 80 | 81 | var state = { 82 | page: 0, 83 | filter: nextProps.filter 84 | } 85 | 86 | this.updateStateWithExternalResults(state, function(updatedState) { 87 | //if filter is null or undefined reset the filter. 88 | if (_.isUndefined(nextProps.filter) || _.isNull(nextProps.filter) || _.isEmpty(nextProps.filter)){ 89 | updatedState.filter = nextProps.filter; 90 | updatedState.filteredResults = null; 91 | } 92 | 93 | // Set the state. 94 | that.setState(updatedState); 95 | }); 96 | }, 97 | 98 | 99 | /** 100 | * Utility function 101 | */ 102 | setDefault: function(original, value){ 103 | return typeof original === 'undefined' ? value : original; 104 | }, 105 | 106 | 107 | /** 108 | * 109 | */ 110 | setPage: function(index, pageSize){ 111 | //This should interact with the data source to get the page at the given index 112 | var that = this; 113 | var state = { 114 | page: index, 115 | pageSize: this.setDefault(pageSize, this.state.pageSize) 116 | }; 117 | 118 | this.updateStateWithExternalResults(state, function(updatedState) { 119 | that.setState(updatedState); 120 | }); 121 | }, 122 | 123 | /** 124 | * 125 | */ 126 | getExternalResults: function(state, callback) { 127 | var filter, 128 | sortColumn, 129 | sortAscending, 130 | page, 131 | pageSize; 132 | 133 | // Fill the search properties. 134 | if (state !== undefined && state.filter !== undefined) { 135 | filter = state.filter; 136 | } else { 137 | filter = this.state.filter; 138 | } 139 | 140 | if (state !== undefined && state.sortColumn !== undefined) { 141 | sortColumn = state.sortColumn; 142 | } else { 143 | sortColumn = this.state.sortColumn; 144 | } 145 | 146 | sortColumn = _.isEmpty(sortColumn) ? this.props.initialSort : sortColumn; 147 | 148 | if (state !== undefined && state.sortAscending !== undefined) { 149 | sortAscending = state.sortAscending; 150 | } else { 151 | sortAscending = this.state.sortAscending; 152 | } 153 | 154 | if (state !== undefined && state.page !== undefined) { 155 | page = state.page; 156 | } else { 157 | page = this.state.page; 158 | } 159 | 160 | if (state !== undefined && state.pageSize !== undefined) { 161 | pageSize = state.pageSize; 162 | } else { 163 | pageSize = this.state.pageSize; 164 | } 165 | 166 | // Obtain the results 167 | this.props.getExternalResults(filter, sortColumn, sortAscending, page, pageSize, callback); 168 | }, 169 | 170 | 171 | /** 172 | * 173 | */ 174 | updateStateWithExternalResults: function(state, callback) { 175 | var that = this; 176 | 177 | // Update the table to indicate that it's loading. 178 | this.setState({ isLoading: true }); 179 | // Grab the results. 180 | this.getExternalResults(state, function(externalResults) { 181 | // Fill the state result properties 182 | if (that.props.enableInfiniteScroll && that.state.results) { 183 | state.results = that.state.results.concat(externalResults.results); 184 | } else { 185 | state.results = externalResults.results; 186 | } 187 | 188 | state.totalResults = externalResults.totalResults; 189 | state.maxPage = that.getMaxPage(externalResults.pageSize, externalResults.totalResults); 190 | state.isLoading = false; 191 | 192 | // If the current page is larger than the max page, reset the page. 193 | if (state.page >= state.maxPage) { 194 | state.page = state.maxPage - 1; 195 | } 196 | 197 | callback(state); 198 | }); 199 | }, 200 | 201 | 202 | /** 203 | * 204 | */ 205 | getMaxPage: function(pageSize, totalResults){ 206 | if (!totalResults) { 207 | totalResults = this.state.totalResults; 208 | } 209 | 210 | var maxPage = Math.ceil(totalResults / pageSize); 211 | return maxPage; 212 | }, 213 | 214 | 215 | /** 216 | * 217 | */ 218 | hasExternalResults: function() { 219 | return typeof(this.props.getExternalResults) === 'function'; 220 | }, 221 | 222 | 223 | /** 224 | * 225 | */ 226 | changeSort: function(sort, sortAscending){ 227 | var that = this; 228 | 229 | // This should change the sort for the given column 230 | var state = { 231 | page:0, 232 | sortColumn: sort, 233 | sortAscending: sortAscending 234 | }; 235 | 236 | this.updateStateWithExternalResults(state, function(updatedState) { 237 | that.setState(updatedState); 238 | }); 239 | }, 240 | 241 | setFilter: function(filter) { 242 | // no-op 243 | }, 244 | 245 | 246 | /** 247 | * 248 | */ 249 | setPageSize: function(size){ 250 | this.setPage(0, size); 251 | }, 252 | 253 | 254 | /** 255 | * 256 | */ 257 | render: function(){ 258 | return 275 | } 276 | }); 277 | 278 | module.exports = GriddleWithCallback; 279 | -------------------------------------------------------------------------------- /example/demo/css/griddle.css: -------------------------------------------------------------------------------- 1 | .griddle-container{ 2 | border: 1px solid #DFDFDF; 3 | border-radius: 0px; 4 | } 5 | 6 | .griddle thead { 7 | background-color: #E0E0E0; 8 | } 9 | 10 | .griddle .top-section{ 11 | clear:both; 12 | display:table; 13 | width:100%; 14 | } 15 | 16 | .griddle .griddle-filter { 17 | float:left; 18 | width:50%; 19 | text-align:left; 20 | color:#222; 21 | min-height:1px; 22 | margin-top:0px; 23 | padding-top:0px; 24 | margin-bottom:10px; 25 | margin-bottom:10px; 26 | } 27 | 28 | .filter-container { 29 | clear: both; 30 | margin: 0px; 31 | width: 100%; 32 | } 33 | 34 | .griddle .griddle-settings-toggle { 35 | float:left; 36 | width:50%; 37 | text-align:right; 38 | } 39 | 40 | .griddle .griddle-settings{ 41 | background-color:#FFF; 42 | border:1px solid #DDD; 43 | color:#222; 44 | padding:10px; 45 | margin-bottom:10px; 46 | } 47 | 48 | 49 | .griddle .griddle-settings .griddle-columns{ 50 | clear:both; 51 | display:table-row; 52 | width:100%; 53 | border-bottom:1px solid #EDEDED; 54 | margin-bottom:10px; 55 | } 56 | 57 | .griddle .griddle-settings .griddle-columns .griddle-column-selection:first-child:before { 58 | content: 'Show/hide columns:'; 59 | } 60 | 61 | .griddle .griddle-settings .griddle-column-selection { 62 | margin-top:0px; 63 | float:left; 64 | } 65 | 66 | /* Get rid of "Settings" text when we view the settings */ 67 | .griddle .griddle-settings h6 { 68 | display: none; 69 | } 70 | 71 | /* Convert settings from checkboxes to links */ 72 | .griddle .griddle-settings input[type=checkbox] { 73 | display: none; 74 | } 75 | .griddle .griddle-settings input[type=checkbox] + span { 76 | color: #aaa; 77 | } 78 | .griddle .griddle-settings input[type=checkbox]:checked + span { 79 | color: #000; 80 | text-decoration: underline; 81 | } 82 | 83 | 84 | .griddle table { 85 | width:100%;table-layout:fixed; 86 | margin-bottom:0px; 87 | } 88 | 89 | .griddle th { 90 | cursor: pointer; 91 | background-color:#E0E0E0; 92 | border:0px; 93 | border:1px solid #DDD; 94 | color:#222; 95 | } 96 | 97 | .griddle td { 98 | border: 1px solid #DDD; 99 | color:#222; 100 | font-size: 90%; 101 | } 102 | 103 | 104 | /* Compress size */ 105 | .griddle table>tbody>tr>td, .griddle .table>thead>tr>th { 106 | padding: 2px; 107 | } 108 | 109 | /* Make alternating rows different colors */ 110 | .griddle table tbody tr:nth-child(even) { 111 | background-color: #F0F0F0; 112 | } 113 | 114 | /* High-light row on hover */ 115 | .griddle table tbody tr:hover { 116 | background-color:#D0D0E0; 117 | } 118 | 119 | 120 | .griddle .footer-container { 121 | font-size: 90%; 122 | background-color: #E0E0E0; 123 | padding:0px; 124 | color:#222; 125 | } 126 | 127 | .griddle .griddle-previous, .griddle .griddle-page, .griddle .griddle-next{ 128 | float:left; 129 | width:33%; 130 | min-height:1px; 131 | margin-top:5px; 132 | } 133 | 134 | .griddle .griddle-page{ 135 | text-align:center; 136 | } 137 | 138 | .griddle .griddle-next{ 139 | text-align:right; 140 | } 141 | 142 | .griddle button { 143 | border:none; 144 | background:none; 145 | margin:0 10px 0 0; 146 | font-weight: bold; 147 | } 148 | 149 | .griddle button:focus {outline:0;} 150 | 151 | /* Fix overflows */ 152 | .griddle .standard-row td{ 153 | word-wrap : break-word; 154 | overflow: hidden; 155 | } 156 | -------------------------------------------------------------------------------- /example/demo/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | margin-bottom: 60px; 8 | background-color: #BFCDE3; 9 | } 10 | 11 | body, 12 | h1,h2,h3,h4,h5,h6 { 13 | font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; 14 | font-weight: 400; 15 | } 16 | 17 | .navbar { 18 | margin-top: 50px; 19 | } 20 | 21 | 22 | /******************************************************************************/ 23 | /* Filter tokenizer */ 24 | 25 | 26 | .filter-tokenizer { 27 | width:100%; 28 | border: 1px solid #ccc; 29 | border-radius: 5px; 30 | padding:10px; 31 | font-size: 90%; 32 | display:table; 33 | } 34 | 35 | .filter-tokenizer:first-child { 36 | padding-left: 0; 37 | padding-top:0; 38 | padding-bottom:0; 39 | } 40 | 41 | .filter-tokenizer .input-group-addon { 42 | border: 0px; 43 | border-right: 1px solid #ccc; 44 | border-top-right-radius: 0; 45 | border-bottom-right-radius: 0; 46 | display: table-cell; 47 | } 48 | 49 | .filter-tokenizer .token-collection { 50 | display: table-cell; 51 | } 52 | 53 | .filter-tokenizer .typeahead { 54 | overflow:auto; 55 | display:block; 56 | } 57 | 58 | .filter-tokenizer .typeahead-token { 59 | display: block; 60 | float:left; 61 | 62 | background-color: #e8e8e8; 63 | background-image: linear-gradient( 64 | #f0f0f0, #e0e0e0 65 | ); 66 | border: 1px solid #ccc; 67 | border-radius: 2px; 68 | 69 | margin:3px; 70 | padding:5px; 71 | 72 | font-weight:bold; 73 | } 74 | 75 | 76 | .filter-input-group { 77 | border: 0px; 78 | border-radius: 2px; 79 | 80 | overflow:auto; 81 | display:block; 82 | 83 | margin:3px; 84 | padding:5px; 85 | } 86 | 87 | .filter-input-group .filter-category, .filter-input-group .filter-operator, .filter-input-group .filter-value { 88 | overflow:auto; 89 | display: block; 90 | float:left; 91 | font-weight: bold; 92 | margin-right: 5px; 93 | } 94 | 95 | .filter-tokenizer .typeahead input { 96 | outline:0; 97 | border:0px; 98 | width:100%; 99 | } 100 | 101 | .filter-tokenizer .typeahead input:focus {outline:0;} 102 | 103 | .filter-tokenizer ul.typeahead-selector { 104 | z-index:100; 105 | position:absolute; 106 | list-style: none; 107 | margin: 0px; 108 | padding: 0px; 109 | background-color:#f8f8f8; 110 | border: 1px solid #222; 111 | width:200px; 112 | max-height:200px; 113 | overflow-y:auto; 114 | 115 | box-shadow: 5px 5px 5px #888888; 116 | } 117 | 118 | .filter-tokenizer ul.typeahead-selector li { 119 | z-index:9999; 120 | border-bottom: 1px solid #ccc; 121 | background-image: linear-gradient( 122 | #ffffff, #f0f0f0 123 | ); 124 | } 125 | 126 | .filter-tokenizer ul.typeahead-selector li.header { 127 | background-image: none; 128 | background-color: #B0B0B0; 129 | color: #ffffff; 130 | font-weight: bold; 131 | padding:5px; 132 | } 133 | 134 | 135 | .filter-tokenizer ul.typeahead-selector li a { 136 | color: #000; 137 | padding:5px; 138 | width:100%; 139 | display:block; 140 | } 141 | 142 | .filter-tokenizer ul.typeahead-selector li a:hover, .filter-tokenizer ul.typeahead-selector .hover a { 143 | text-decoration: none; 144 | background-color: #00f; 145 | color: #fff; 146 | } 147 | 148 | /******************************************************************************/ 149 | .datepicker__triangle { 150 | margin-top: -8px; 151 | margin-left: -8px; 152 | } 153 | .datepicker__triangle, .datepicker__triangle:before { 154 | box-sizing: content-box; 155 | position: absolute; 156 | border: 8px solid transparent; 157 | height: 0; 158 | width: 1px; 159 | border-top: none; 160 | border-bottom-color: #f0f0f0; 161 | } 162 | .datepicker__triangle:before { 163 | content: ""; 164 | z-index: -1; 165 | border-width: 8px; 166 | top: -1px; 167 | left: -8px; 168 | border-bottom-color: #aeaeae; 169 | } 170 | 171 | .datepicker { 172 | font-size: 11px; 173 | background-color: #fff; 174 | color: #000; 175 | border: 1px solid #aeaeae; 176 | border-radius: 4px; 177 | display: inline-block; 178 | position: relative; 179 | 180 | box-shadow: 5px 5px 5px #888888; 181 | } 182 | 183 | .datepicker__container { 184 | position: absolute; 185 | display: inline-block; 186 | z-index: 2147483647; 187 | } 188 | 189 | .datepicker__triangle { 190 | position: absolute; 191 | left: 50px; 192 | } 193 | 194 | .datepicker__header { 195 | text-align: center; 196 | background-color: #f0f0f0; 197 | border-bottom: 1px solid #aeaeae; 198 | border-top-left-radius: 4px; 199 | border-top-right-radius: 4px; 200 | padding-top: 8px; 201 | position: relative; 202 | } 203 | 204 | .datepicker__current-month { 205 | color: black; 206 | font-weight: bold; 207 | font-size: 13px; 208 | } 209 | 210 | .datepicker__navigation { 211 | line-height: 24px; 212 | text-align: center; 213 | cursor: pointer; 214 | position: absolute; 215 | top: 10px; 216 | width: 0; 217 | border: 6px solid transparent; 218 | } 219 | .datepicker__navigation--previous { 220 | left: 10px; 221 | border-right-color: #ccc; 222 | } 223 | .datepicker__navigation--previous:hover { 224 | border-right-color: #b3b3b3; 225 | } 226 | .datepicker__navigation--next { 227 | right: 10px; 228 | border-left-color: #ccc; 229 | } 230 | .datepicker__navigation--next:hover { 231 | border-left-color: #b3b3b3; 232 | } 233 | 234 | .datepicker__week-day { 235 | color: #ccc; 236 | display: inline-block; 237 | width: 28px; 238 | line-height: 24px; 239 | } 240 | 241 | .datepicker__month { 242 | margin: 5px; 243 | text-align: center; 244 | } 245 | 246 | .datepicker__day { 247 | color: #000; 248 | display: inline-block; 249 | width: 24px; 250 | line-height: 24px; 251 | text-align: center; 252 | margin: 2px; 253 | cursor: pointer; 254 | } 255 | .datepicker__day:hover { 256 | border-radius: 4px; 257 | background-color: #f0f0f0; 258 | } 259 | .datepicker__day--today { 260 | font-weight: bold; 261 | } 262 | .datepicker__day--selected { 263 | border-radius: 4px; 264 | background-color: #216ba5; 265 | color: #fff; 266 | } 267 | .datepicker__day--selected:hover { 268 | background-color: #1d5d90; 269 | } 270 | .datepicker__day--disabled { 271 | cursor: default; 272 | color: #ccc; 273 | } 274 | .datepicker__day--disabled:hover { 275 | background-color: transparent; 276 | } 277 | 278 | .datepicker__input { 279 | position: relative; 280 | line-height: 16px; 281 | } 282 | .datepicker__input:focus { 283 | outline: none; 284 | } 285 | -------------------------------------------------------------------------------- /example/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react-structured-filter 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var ExampleTable = require('./ExampleTable.jsx'); 4 | 5 | React.render( 6 |
7 | 8 |
9 |
10 |
11 | react-structured-filter demo 12 |
13 |
14 |
15 | 16 |
17 |
18 |

Example stock data

19 | 20 |
21 |

Documentation

22 | 23 |
24 |
25 |
26 | , 27 | document.getElementById('main')); 28 | -------------------------------------------------------------------------------- /example/taffy-min.js: -------------------------------------------------------------------------------- 1 | var TAFFY,exports,T;(function(){var f,q,p,t,d,b,n,m,r,e,c,u,w,v,h,g,j,o,i,l,a,s,k;if(!TAFFY){d="2.7";b=1;n="000000";m=1000;r={};e=function(x){if(TAFFY.isArray(x)||TAFFY.isObject(x)){return x}else{return JSON.parse(x)}};i=function(y,x){return l(y,function(z){return x.indexOf(z)>=0})};l=function(A,z,y){var x=[];if(A==null){return x}if(Array.prototype.filter&&A.filter===Array.prototype.filter){return A.filter(z,y)}c(A,function(D,B,C){if(z.call(y,D,B,C)){x[x.length]=D}});return x};k=function(x){return Object.prototype.toString.call(x)==="[object RegExp]"};s=function(z){var x=T.isArray(z)?[]:T.isObject(z)?{}:null;if(z===null){return z}for(var y in z){x[y]=k(z[y])?z[y].toString():T.isArray(z[y])||T.isObject(z[y])?s(z[y]):z[y]}return x};a=function(y){var x=JSON.stringify(y);if(x.match(/regex/)===null){return x}return JSON.stringify(s(y))};c=function(B,A,C){var E,D,z,F;if(B&&((T.isArray(B)&&B.length===1)||(!T.isArray(B)))){A((T.isArray(B))?B[0]:B,0)}else{for(E,D,z=0,B=(T.isArray(B))?B:[B],F=B.length;z4){c(A,function(B){if(h(z,B)){x=true}})}break}});return x};v=function(y){var x=[];if(T.isString(y)&&/[t][0-9]*[r][0-9]*/i.test(y)){y={___id:y}}if(T.isArray(y)){c(y,function(z){x.push(v(z))});y=function(){var A=this,z=false;c(x,function(B){if(h(A,B)){z=true}});return z};return y}if(T.isObject(y)){if(T.isObject(y)&&y.___id&&y.___s){y={___id:y.___id}}u(y,function(z,A){if(!T.isObject(z)){z={is:z}}u(z,function(B,C){var E=[],D;D=(C==="hasAll")?function(F,G){G(F)}:c;D(B,function(G){var F=true,H=false,I;I=function(){var N=this[A],M="==",O="!=",Q="===",R="<",L=">",S="<=",P=">=",K="!==",J;if(typeof N==="undefined"){return false}if((C.indexOf("!")===0)&&C!==O&&C!==K){F=false;C=C.substring(1,C.length)}J=((C==="regex")?(G.test(N)):(C==="lt"||C===R)?(NG):(C==="lte"||C===S)?(N<=G):(C==="gte"||C===P)?(N>=G):(C==="left")?(N.indexOf(G)===0):(C==="leftnocase")?(N.toLowerCase().indexOf(G.toLowerCase())===0):(C==="right")?(N.substring((N.length-G.length))===G):(C==="rightnocase")?(N.toLowerCase().substring((N.length-G.length))===G.toLowerCase()):(C==="like")?(N.indexOf(G)>=0):(C==="likenocase")?(N.toLowerCase().indexOf(G.toLowerCase())>=0):(C===Q||C==="is")?(N===G):(C===M)?(N==G):(C===K)?(N!==G):(C===O)?(N!=G):(C==="isnocase")?(N.toLowerCase?N.toLowerCase()===G.toLowerCase():N===G):(C==="has")?(T.has(N,G)):(C==="hasall")?(T.hasAll(N,G)):(C==="contains")?(TAFFY.isArray(N)&&N.indexOf(G)>-1):(C.indexOf("is")===-1&&!TAFFY.isNull(N)&&!TAFFY.isUndefined(N)&&!TAFFY.isObject(G)&&!TAFFY.isArray(G))?(G===N[C]):(T[C]&&T.isFunction(T[C])&&C.indexOf("is")===0)?T[C](N)===G:(T[C]&&T.isFunction(T[C]))?T[C](N,G):(false));J=(J&&!F)?false:(!J&&!F)?true:J;return J};E.push(I)});if(E.length===1){x.push(E[0])}else{x.push(function(){var G=this,F=false;c(E,function(H){if(H.apply(G)){F=true}});return F})}})});y=function(){var A=this,z=true;z=(x.length===1&&!x[0].apply(A))?false:(x.length===2&&(!x[0].apply(A)||!x[1].apply(A)))?false:(x.length===3&&(!x[0].apply(A)||!x[1].apply(A)||!x[2].apply(A)))?false:(x.length===4&&(!x[0].apply(A)||!x[1].apply(A)||!x[2].apply(A)||!x[3].apply(A)))?false:true;if(x.length>4){c(x,function(B){if(!h(A,B)){z=false}})}return z};return y}if(T.isFunction(y)){return y}};j=function(x,y){var z=function(B,A){var C=0;T.each(y,function(F){var H,E,D,I,G;H=F.split(" ");E=H[0];D=(H.length===1)?"logical":H[1];if(D==="logical"){I=g(B[E]);G=g(A[E]);T.each((I.length<=G.length)?I:G,function(J,K){if(I[K]G[K]){C=1;return TAFFY.EXIT}}})}else{if(D==="logicaldesc"){I=g(B[E]);G=g(A[E]);T.each((I.length<=G.length)?I:G,function(J,K){if(I[K]>G[K]){C=-1;return TAFFY.EXIT}else{if(I[K]A[E]){C=1;return T.EXIT}else{if(D==="desc"&&B[E]>A[E]){C=-1;return T.EXIT}else{if(D==="desc"&&B[E]G.length){C=1}else{if(C===0&&D==="logicaldesc"&&I.length>G.length){C=-1}else{if(C===0&&D==="logicaldesc"&&I.lengthm){x={};y=0}return x["_"+z]||(function(){var D=String(z),C=[],G="_",B="",A,E,F;for(A=0,E=D.length;A=48&&F<=57)||F===46){if(B!=="n"){B="n";C.push(G.toLowerCase());G=""}G=G+D.charAt(A)}else{if(B!=="s"){B="s";C.push(parseFloat(G));G=""}G=G+D.charAt(A)}}C.push((B==="n")?parseFloat(G):G.toLowerCase());C.shift();x["_"+z]=C;y++;return C}())}}());o=function(){this.context({results:this.getDBI().query(this.context())})};r.extend("filter",function(){var y=TAFFY.mergeObj(this.context(),{run:null}),x=[];c(y.q,function(z){x.push(z)});y.q=x;c(arguments,function(z){y.q.push(v(z));y.filterRaw.push(z)});return this.getroot(y)});r.extend("order",function(z){z=z.split(",");var y=[],A;c(z,function(x){y.push(x.replace(/^\s*/,"").replace(/\s*$/,""))});A=TAFFY.mergeObj(this.context(),{sort:null});A.order=y;return this.getroot(A)});r.extend("limit",function(z){var y=TAFFY.mergeObj(this.context(),{}),x;y.limit=z;if(y.run&&y.sort){x=[];c(y.results,function(B,A){if((A+1)>z){return TAFFY.EXIT}x.push(B)});y.results=x}return this.getroot(y)});r.extend("start",function(z){var y=TAFFY.mergeObj(this.context(),{}),x;y.start=z;if(y.run&&y.sort&&!y.limit){x=[];c(y.results,function(B,A){if((A+1)>z){x.push(B)}});y.results=x}else{y=TAFFY.mergeObj(this.context(),{run:null,start:z})}return this.getroot(y)});r.extend("update",function(A,z,x){var B=true,D={},y=arguments,C;if(TAFFY.isString(A)&&(arguments.length===2||arguments.length===3)){D[A]=z;if(arguments.length===3){B=x}}else{D=A;if(y.length===2){B=z}}C=this;o.call(this);c(this.context().results,function(E){var F=D;if(TAFFY.isFunction(F)){F=F.apply(TAFFY.mergeObj(E,{}))}else{if(T.isFunction(F)){F=F(TAFFY.mergeObj(E,{}))}}if(TAFFY.isObject(F)){C.getDBI().update(E.___id,F,B)}});if(this.context().results.length){this.context({run:null})}return this});r.extend("remove",function(x){var y=this,z=0;o.call(this);c(this.context().results,function(A){y.getDBI().remove(A.___id);z++});if(this.context().results.length){this.context({run:null});y.getDBI().removeCommit(x)}return z});r.extend("count",function(){o.call(this);return this.context().results.length});r.extend("callback",function(z,x){if(z){var y=this;setTimeout(function(){o.call(y);z.call(y.getroot(y.context()))},x||0)}return null});r.extend("get",function(){o.call(this);return this.context().results});r.extend("stringify",function(){return JSON.stringify(this.get())});r.extend("first",function(){o.call(this);return this.context().results[0]||false});r.extend("last",function(){o.call(this);return this.context().results[this.context().results.length-1]||false});r.extend("sum",function(){var y=0,x=this;o.call(x);c(arguments,function(z){c(x.context().results,function(A){y=y+(A[z]||0)})});return y});r.extend("min",function(y){var x=null;o.call(this);c(this.context().results,function(z){if(x===null||z[y]":return C>F;case"<=":return C<=F;case">=":return C>=F;case"==":return C==F;case"!=":return C!=F;default:throw String(H)+" is not supported"}};y=function(C,F){var B={},D,E;for(D in C){if(C.hasOwnProperty(D)){B[D]=C[D]}}for(D in F){if(F.hasOwnProperty(D)&&D!=="___id"&&D!=="___s"){E=!TAFFY.isUndefined(B[D])?"right_":"";B[E+String(D)]=F[D]}}return B};z=function(F){var B,D,C=arguments,E=C.length,G=[];if(typeof F.filter!=="function"){if(F.TAFFY){B=F()}else{throw"TAFFY DB or result not supplied"}}else{B=F}this.context({results:this.getDBI().query(this.context())});TAFFY.each(this.context().results,function(H){B.each(function(K){var I,J=true;CONDITION:for(D=1;Dx){x=z[y]}});return x});r.extend("select",function(){var y=[],x=arguments;o.call(this);if(arguments.length===1){c(this.context().results,function(z){y.push(z[x[0]])})}else{c(this.context().results,function(z){var A=[];c(x,function(B){A.push(z[B])});y.push(A)})}return y});r.extend("distinct",function(){var y=[],x=arguments;o.call(this);if(arguments.length===1){c(this.context().results,function(A){var z=A[x[0]],B=false;c(y,function(C){if(z===C){B=true;return TAFFY.EXIT}});if(!B){y.push(z)}})}else{c(this.context().results,function(z){var B=[],A=false;c(x,function(C){B.push(z[C])});c(y,function(D){var C=true;c(x,function(F,E){if(B[E]!==D[E]){C=false;return TAFFY.EXIT}});if(C){A=true;return TAFFY.EXIT}});if(!A){y.push(B)}})}return y});r.extend("supplant",function(y,x){var z=[];o.call(this);c(this.context().results,function(A){z.push(y.replace(/\{([^\{\}]*)\}/g,function(C,B){var D=A[B];return typeof D==="string"||typeof D==="number"?D:C}))});return(!x)?z.join(""):z});r.extend("each",function(x){o.call(this);c(this.context().results,x);return this});r.extend("map",function(x){var y=[];o.call(this);c(this.context().results,function(z){y.push(x(z))});return y});T=function(F){var C=[],G={},D=1,z={template:false,onInsert:false,onUpdate:false,onRemove:false,onDBChange:false,storageName:false,forcePropertyCase:null,cacheSize:100,name:""},B=new Date(),A=0,y=0,I={},E,x,H;x=function(L){var K=[],J=false;if(L.length===0){return C}c(L,function(M){if(T.isString(M)&&/[t][0-9]*[r][0-9]*/i.test(M)&&C[G[M]]){K.push(C[G[M]]);J=true}if(T.isObject(M)&&M.___id&&M.___s&&C[G[M.___id]]){K.push(C[G[M.___id]]);J=true}if(T.isArray(M)){c(M,function(N){c(x(N),function(O){K.push(O)})})}});if(J&&K.length>1){K=[]}return K};E={dm:function(J){if(J){B=J;I={};A=0;y=0}if(z.onDBChange){setTimeout(function(){z.onDBChange.call(C)},0)}if(z.storageName){setTimeout(function(){localStorage.setItem("taffy_"+z.storageName,JSON.stringify(C))})}return B},insert:function(M,N){var L=[],K=[],J=e(M);c(J,function(P,Q){var O,R;if(T.isArray(P)&&Q===0){c(P,function(S){L.push((z.forcePropertyCase==="lower")?S.toLowerCase():(z.forcePropertyCase==="upper")?S.toUpperCase():S)});return true}else{if(T.isArray(P)){O={};c(P,function(U,S){O[L[S]]=U});P=O}else{if(T.isObject(P)&&z.forcePropertyCase){R={};u(P,function(U,S){R[(z.forcePropertyCase==="lower")?S.toLowerCase():(z.forcePropertyCase==="upper")?S.toUpperCase():S]=P[S]});P=R}}}D++;P.___id="T"+String(n+b).slice(-6)+"R"+String(n+D).slice(-6);P.___s=true;K.push(P.___id);if(z.template){P=T.mergeObj(z.template,P)}C.push(P);G[P.___id]=C.length-1;if(z.onInsert&&(N||TAFFY.isUndefined(N))){z.onInsert.call(P)}E.dm(new Date())});return H(K)},sort:function(J){C=j(C,J.split(","));G={};c(C,function(L,K){G[L.___id]=K});E.dm(new Date());return true},update:function(Q,M,L){var P={},O,N,J,K;if(z.forcePropertyCase){u(M,function(R,S){P[(z.forcePropertyCase==="lower")?S.toLowerCase():(z.forcePropertyCase==="upper")?S.toUpperCase():S]=R});M=P}O=C[G[Q]];N=T.mergeObj(O,M);J={};K=false;u(N,function(R,S){if(TAFFY.isUndefined(O[S])||O[S]!==R){J[S]=R;K=true}});if(K){if(z.onUpdate&&(L||TAFFY.isUndefined(L))){z.onUpdate.call(N,C[G[Q]],J)}C[G[Q]]=N;E.dm(new Date())}},remove:function(J){C[G[J]].___s=false},removeCommit:function(K){var J;for(J=C.length-1;J>-1;J--){if(!C[J].___s){if(z.onRemove&&(K||TAFFY.isUndefined(K))){z.onRemove.call(C[J])}G[C[J].___id]=undefined;C.splice(J,1)}}G={};c(C,function(M,L){G[M.___id]=L});E.dm(new Date())},query:function(L){var O,P,K,N,M,J;if(z.cacheSize){P="";c(L.filterRaw,function(Q){if(T.isFunction(Q)){P="nocache";return TAFFY.EXIT}});if(P===""){P=a(T.mergeObj(L,{q:false,run:false,sort:false}))}}if(!L.results||!L.run||(L.run&&E.dm()>L.run)){K=[];if(z.cacheSize&&I[P]){I[P].i=A++;return I[P].results}else{if(L.q.length===0&&L.index.length===0){c(C,function(Q){K.push(Q)});O=K}else{N=x(L.index);c(N,function(Q){if(L.q.length===0||h(Q,L.q)){K.push(Q)}});O=K}}}else{O=L.results}if(L.order.length>0&&(!L.run||!L.sort)){O=j(O,L.order)}if(O.length&&((L.limit&&L.limit=L.start)){if(L.limit){J=(L.start)?(Q+1)-L.start:Q;if(JL.limit){return TAFFY.EXIT}}}else{M.push(R)}}});O=M}if(z.cacheSize&&P!=="nocache"){y++;setTimeout(function(){var Q,R;if(y>=z.cacheSize*2){y=0;Q=A-z.cacheSize;R={};u(function(U,S){if(U.i>=Q){R[S]=U}});I=R}},0);I[P]={i:A++,results:O}}return O}};H=function(){var K,J;K=TAFFY.mergeObj(TAFFY.mergeObj(r,{insert:undefined}),{getDBI:function(){return E},getroot:function(L){return H.call(L)},context:function(L){if(L){J=TAFFY.mergeObj(J,L.hasOwnProperty("results")?TAFFY.mergeObj(L,{run:new Date(),sort:new Date()}):L)}return J},extend:undefined});J=(this&&this.q)?this:{limit:false,start:false,q:[],filterRaw:[],index:[],order:[],results:false,run:null,sort:null,settings:z};c(arguments,function(L){if(w(L)){J.index.push(L)}else{J.q.push(v(L))}J.filterRaw.push(L)});return K};b++;if(F){E.insert(F)}H.insert=E.insert;H.merge=function(M,L,N){var K={},J=[],O={};N=N||false;L=L||"id";c(M,function(Q){var P;K[L]=Q[L];J.push(Q[L]);P=H(K).first();if(P){E.update(P.___id,Q,N)}else{E.insert(Q,N)}});O[L]=J;return H(O)};H.TAFFY=true;H.sort=E.sort;H.settings=function(J){if(J){z=TAFFY.mergeObj(z,J);if(J.template){H().update(J.template)}}return z};H.store=function(L){var K=false,J;if(localStorage){if(L){J=localStorage.getItem("taffy_"+L);if(J&&J.length>0){H.insert(J);K=true}if(C.length>0){setTimeout(function(){localStorage.setItem("taffy_"+z.storageName,JSON.stringify(C))})}}H.settings({storageName:L})}return H};return H};TAFFY=T;T.each=c;T.eachin=u;T.extend=r.extend;TAFFY.EXIT="TAFFYEXIT";TAFFY.mergeObj=function(z,x){var y={};u(z,function(A,B){y[B]=z[B]});u(x,function(A,B){y[B]=x[B]});return y};TAFFY.has=function(z,y){var x=false,A;if((z.TAFFY)){x=z(y);if(x.length>0){return true}else{return false}}else{switch(T.typeOf(z)){case"object":if(T.isObject(y)){u(y,function(B,C){if(x===true&&!T.isUndefined(z[C])&&z.hasOwnProperty(C)){x=T.has(z[C],y[C])}else{x=false;return TAFFY.EXIT}})}else{if(T.isArray(y)){c(y,function(B,C){x=T.has(z,y[C]);if(x){return TAFFY.EXIT}})}else{if(T.isString(y)){if(!TAFFY.isUndefined(z[y])){return true}else{return false}}}}return x;case"array":if(T.isObject(y)){c(z,function(B,C){x=T.has(z[C],y);if(x===true){return TAFFY.EXIT}})}else{if(T.isArray(y)){c(y,function(C,B){c(z,function(E,D){x=T.has(z[D],y[B]);if(x===true){return TAFFY.EXIT}});if(x===true){return TAFFY.EXIT}})}else{if(T.isString(y)||T.isNumber(y)){x=false;for(A=0;A 48 | {this.days(weekStart)} 49 | 50 | ); 51 | }, 52 | 53 | renderDay: function(day, key) { 54 | var minDate = new DateUtil(this.props.minDate).safeClone(), 55 | maxDate = new DateUtil(this.props.maxDate).safeClone(), 56 | disabled = day.isBefore(minDate) || day.isAfter(maxDate); 57 | 58 | return ( 59 | 66 | ); 67 | }, 68 | 69 | days: function(weekStart) { 70 | return weekStart.mapDaysInWeek(this.renderDay); 71 | }, 72 | 73 | render: function() { 74 | return ( 75 |
76 |
77 |
78 | 80 | 81 | 82 | {this.state.date.format("MMMM YYYY")} 83 | 84 | 86 | 87 |
88 |
Mo
89 |
Tu
90 |
We
91 |
Th
92 |
Fr
93 |
Sa
94 |
Su
95 |
96 |
97 |
98 | {this.weeks()} 99 |
100 |
101 | ); 102 | } 103 | }); 104 | 105 | module.exports = Calendar; 106 | -------------------------------------------------------------------------------- /src/react-datepicker/date_input.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react/addons'); 4 | var moment = require('moment'); 5 | 6 | 7 | var DateUtil = require('./util/date'); 8 | 9 | var DateInput = React.createClass({ 10 | propTypes: { 11 | onKeyDown: React.PropTypes.func 12 | }, 13 | 14 | getDefaultProps: function() { 15 | return { 16 | dateFormat: 'YYYY-MM-DD' 17 | }; 18 | }, 19 | 20 | getInitialState: function() { 21 | return { 22 | value: this.safeDateFormat(this.props.date) 23 | }; 24 | }, 25 | 26 | componentDidMount: function() { 27 | this.toggleFocus(this.props.focus); 28 | }, 29 | 30 | componentWillReceiveProps: function(newProps) { 31 | this.toggleFocus(newProps.focus); 32 | 33 | this.setState({ 34 | value: this.safeDateFormat(newProps.date) 35 | }); 36 | }, 37 | 38 | toggleFocus: function(focus) { 39 | if (focus) { 40 | this.refs.entry.getDOMNode().focus(); 41 | } else { 42 | this.refs.entry.getDOMNode().blur(); 43 | } 44 | }, 45 | 46 | handleChange: function(event) { 47 | var date = moment(event.target.value, this.props.dateFormat, true); 48 | 49 | this.setState({ 50 | value: event.target.value 51 | }); 52 | }, 53 | 54 | safeDateFormat: function(date) { 55 | return !! date ? date.format(this.props.dateFormat) : null; 56 | }, 57 | 58 | isValueAValidDate: function() { 59 | var date = moment(event.target.value, this.props.dateFormat, true); 60 | 61 | return date.isValid(); 62 | }, 63 | 64 | handleEnter: function(event) { 65 | if (this.isValueAValidDate()) { 66 | var date = moment(event.target.value, this.props.dateFormat, true); 67 | this.props.setSelected(new DateUtil(date)); 68 | } 69 | }, 70 | 71 | handleKeyDown: function(event) { 72 | switch(event.key) { 73 | case "Enter": 74 | event.preventDefault(); 75 | this.handleEnter(event); 76 | break; 77 | case "Backspace": 78 | this.props.onKeyDown(event); 79 | break; 80 | } 81 | }, 82 | 83 | handleClick: function(event) { 84 | this.props.handleClick(event); 85 | }, 86 | 87 | render: function() { 88 | return ; 98 | } 99 | }); 100 | 101 | module.exports = DateInput; 102 | -------------------------------------------------------------------------------- /src/react-datepicker/datepicker.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react/addons'); 4 | 5 | var Popover = require('./popover'); 6 | var DateUtil = require('./util/date'); 7 | var Calendar = require('./calendar'); 8 | var DateInput = require('./date_input'); 9 | 10 | var DatePicker = React.createClass({ 11 | propTypes: { 12 | onChange: React.PropTypes.func, 13 | onKeyDown: React.PropTypes.func 14 | }, 15 | 16 | getInitialState: function() { 17 | return { 18 | focus: true 19 | }; 20 | }, 21 | 22 | handleFocus: function() { 23 | this.setState({ 24 | focus: true 25 | }); 26 | }, 27 | 28 | hideCalendar: function() { 29 | this.setState({ 30 | focus: false 31 | }); 32 | }, 33 | 34 | handleSelect: function(date) { 35 | this.hideCalendar(); 36 | this.setSelected(date); 37 | }, 38 | 39 | setSelected: function(date) { 40 | this.props.onChange(date.moment()); 41 | }, 42 | 43 | onInputClick: function() { 44 | this.setState({ 45 | focus: true 46 | }); 47 | }, 48 | 49 | calendar: function() { 50 | if (this.state.focus) { 51 | return ( 52 | 53 | 59 | 60 | ); 61 | } 62 | }, 63 | 64 | render: function() { 65 | return ( 66 |
67 | 79 | {this.calendar()} 80 |
81 | ); 82 | } 83 | }); 84 | 85 | module.exports = DatePicker; 86 | -------------------------------------------------------------------------------- /src/react-datepicker/day.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react/addons'); 4 | var moment = require('moment'); 5 | 6 | var Day = React.createClass({ 7 | handleClick: function(event) { 8 | if (this.props.disabled) return; 9 | 10 | this.props.onClick(event); 11 | }, 12 | 13 | render: function() { 14 | classes = React.addons.classSet({ 15 | 'datepicker__day': true, 16 | 'datepicker__day--disabled': this.props.disabled, 17 | 'datepicker__day--selected': this.props.day.sameDay(this.props.selected), 18 | 'datepicker__day--today': this.props.day.sameDay(moment()) 19 | }); 20 | 21 | return ( 22 |
23 | {this.props.day.day()} 24 |
25 | ); 26 | } 27 | }); 28 | 29 | module.exports = Day; 30 | -------------------------------------------------------------------------------- /src/react-datepicker/popover.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react/addons'); 4 | var Tether = require('tether/tether'); 5 | 6 | 7 | var Popover = React.createClass({ 8 | displayName: 'Popover', 9 | 10 | componentWillMount: function() { 11 | popoverContainer = document.createElement('span'); 12 | popoverContainer.className = 'datepicker__container'; 13 | 14 | this._popoverElement = popoverContainer; 15 | 16 | document.querySelector('body').appendChild(this._popoverElement); 17 | }, 18 | 19 | componentDidMount: function() { 20 | this._renderPopover(); 21 | }, 22 | 23 | componentDidUpdate: function() { 24 | this._renderPopover(); 25 | }, 26 | 27 | _popoverComponent: function() { 28 | var className = this.props.className; 29 | return ( 30 |
31 | {this.props.children} 32 |
33 | ); 34 | }, 35 | 36 | _tetherOptions: function() { 37 | return { 38 | element: this._popoverElement, 39 | target: this.getDOMNode().parentElement, 40 | attachment: 'top left', 41 | targetAttachment: 'bottom left', 42 | targetOffset: '10px 0', 43 | optimizations: { 44 | moveElement: false // always moves to anyway! 45 | }, 46 | constraints: [ 47 | { 48 | to: 'window', 49 | attachment: 'together', 50 | pin: true 51 | } 52 | ] 53 | }; 54 | }, 55 | 56 | _renderPopover: function() { 57 | React.render(this._popoverComponent(), this._popoverElement); 58 | 59 | if (this._tether != null) { 60 | this._tether.setOptions(this._tetherOptions()); 61 | } else { 62 | this._tether = new Tether(this._tetherOptions()); 63 | } 64 | }, 65 | 66 | componentWillUnmount: function() { 67 | this._tether.destroy(); 68 | React.unmountComponentAtNode(this._popoverElement); 69 | if (this._popoverElement.parentNode) { 70 | this._popoverElement.parentNode.removeChild(this._popoverElement); 71 | } 72 | }, 73 | 74 | render: function() { 75 | return ; 76 | } 77 | }); 78 | 79 | module.exports = Popover; 80 | -------------------------------------------------------------------------------- /src/react-datepicker/util/date.js: -------------------------------------------------------------------------------- 1 | function DateUtil(date) { 2 | this._date = date; 3 | } 4 | 5 | DateUtil.prototype.isBefore = function(other) { 6 | return this._date.isBefore(other._date, 'day'); 7 | }; 8 | 9 | DateUtil.prototype.isAfter = function(other) { 10 | return this._date.isAfter(other._date, 'day'); 11 | }; 12 | 13 | DateUtil.prototype.sameDay = function(other) { 14 | return this._date.isSame(other._date, 'day'); 15 | }; 16 | 17 | DateUtil.prototype.sameMonth = function(other) { 18 | return this._date.isSame(other._date, 'month'); 19 | }; 20 | 21 | DateUtil.prototype.day = function() { 22 | return this._date.date(); 23 | }; 24 | 25 | DateUtil.prototype.mapDaysInWeek = function(callback) { 26 | var week = []; 27 | var firstDay = this._date.clone().startOf('isoWeek'); 28 | 29 | for(var i = 0; i < 7; i++) { 30 | var day = new DateUtil(firstDay.clone().add(i, 'days')); 31 | 32 | week[i] = callback(day, i); 33 | } 34 | 35 | return week; 36 | }; 37 | 38 | DateUtil.prototype.mapWeeksInMonth = function(callback) { 39 | var month = []; 40 | var firstDay = this._date.clone().startOf('month').startOf('isoWeek'); 41 | 42 | for(var i = 0; i < 6; i++) { 43 | var weekStart = new DateUtil(firstDay.clone().add(i, 'weeks')); 44 | 45 | month[i] = callback(weekStart, i); 46 | } 47 | 48 | return month; 49 | }; 50 | 51 | DateUtil.prototype.weekInMonth = function(other) { 52 | var firstDayInWeek = this._date.clone(); 53 | var lastDayInWeek = this._date.clone().isoWeekday(7); 54 | 55 | return firstDayInWeek.isSame(other._date, 'month') || 56 | lastDayInWeek.isSame(other._date, 'month'); 57 | }; 58 | 59 | DateUtil.prototype.format = function() { 60 | return this._date.format.apply(this._date, arguments); 61 | }; 62 | 63 | DateUtil.prototype.addMonth = function() { 64 | return new DateUtil(this._date.clone().add(1, 'month')); 65 | }; 66 | 67 | DateUtil.prototype.subtractMonth = function() { 68 | return new DateUtil(this._date.clone().subtract(1, 'month')); 69 | }; 70 | 71 | DateUtil.prototype.clone = function() { 72 | return new DateUtil(this._date.clone()); 73 | }; 74 | 75 | DateUtil.prototype.safeClone = function(alternative) { 76 | if (!! this._date) return this.clone(); 77 | 78 | if (alternative === undefined) alternative = null; 79 | return new DateUtil(alternative); 80 | }; 81 | 82 | DateUtil.prototype.moment = function() { 83 | return this._date; 84 | }; 85 | 86 | module.exports = DateUtil; 87 | -------------------------------------------------------------------------------- /src/react-typeahead/keyevent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PolyFills make me sad 3 | */ 4 | var KeyEvent = KeyEvent || {}; 5 | KeyEvent.DOM_VK_UP = KeyEvent.DOM_VK_UP || 38; 6 | KeyEvent.DOM_VK_DOWN = KeyEvent.DOM_VK_DOWN || 40; 7 | KeyEvent.DOM_VK_BACK_SPACE = KeyEvent.DOM_VK_BACK_SPACE || 8; 8 | KeyEvent.DOM_VK_RETURN = KeyEvent.DOM_VK_RETURN || 13; 9 | KeyEvent.DOM_VK_ENTER = KeyEvent.DOM_VK_ENTER || 14; 10 | KeyEvent.DOM_VK_ESCAPE = KeyEvent.DOM_VK_ESCAPE || 27; 11 | KeyEvent.DOM_VK_TAB = KeyEvent.DOM_VK_TAB || 9; 12 | 13 | module.exports = KeyEvent; 14 | -------------------------------------------------------------------------------- /src/react-typeahead/react-typeahead.js: -------------------------------------------------------------------------------- 1 | var Typeahead = require('./typeahead'); 2 | var Tokenizer = require('./tokenizer'); 3 | 4 | module.exports = { 5 | Typeahead: Typeahead, 6 | Tokenizer: Tokenizer 7 | }; 8 | -------------------------------------------------------------------------------- /src/react-typeahead/tokenizer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | var React = window.React || require('react'); 6 | var Token = require('./token'); 7 | var KeyEvent = require('../keyevent'); 8 | var Typeahead = require('../typeahead'); 9 | 10 | /** 11 | * A typeahead that, when an option is selected, instead of simply filling 12 | * the text entry widget, prepends a renderable "token", that may be deleted 13 | * by pressing backspace on the beginning of the line with the keyboard. 14 | */ 15 | var TypeaheadTokenizer = React.createClass({ 16 | propTypes: { 17 | options: React.PropTypes.array, 18 | customClasses: React.PropTypes.object, 19 | defaultSelected: React.PropTypes.array, 20 | defaultValue: React.PropTypes.string, 21 | placeholder: React.PropTypes.string, 22 | onTokenRemove: React.PropTypes.func, 23 | onTokenAdd: React.PropTypes.func 24 | }, 25 | 26 | getInitialState: function() { 27 | return { 28 | selected: this.props.defaultSelected, 29 | category: "", 30 | operator: "" 31 | }; 32 | }, 33 | 34 | getDefaultProps: function() { 35 | return { 36 | options: [], 37 | defaultSelected: [], 38 | customClasses: {}, 39 | defaultValue: "", 40 | placeholder: "", 41 | onTokenAdd: function() {}, 42 | onTokenRemove: function() {} 43 | }; 44 | }, 45 | 46 | // TODO: Support initialized tokens 47 | // 48 | _renderTokens: function() { 49 | var tokenClasses = {} 50 | tokenClasses[this.props.customClasses.token] = !!this.props.customClasses.token; 51 | var classList = React.addons.classSet(tokenClasses); 52 | var result = this.state.selected.map(function(selected) { 53 | mykey = selected.category + selected.operator + selected.value; 54 | 55 | return ( 56 | 58 | { selected } 59 | 60 | 61 | ) 62 | }, this); 63 | return result; 64 | }, 65 | 66 | _getOptionsForTypeahead: function() { 67 | if (this.state.category=="") { 68 | var categories=[]; 69 | for (var i = 0; i < this.props.options.length; i++) { 70 | categories.push(this.props.options[i].category); 71 | } 72 | return categories; 73 | } else if (this.state.operator=="") { 74 | categoryType = this._getCategoryType(); 75 | 76 | if (categoryType == "text") { return ["==", "!=", "contains", "!contains"];} 77 | else if (categoryType == "textoptions") {return ["==", "!="];} 78 | else if (categoryType == "number" || categoryType == "date") {return ["==", "!=", "<", "<=", ">", ">="];} 79 | else {console.log("WARNING: Unknown category type in tokenizer");}; 80 | 81 | } else { 82 | var options = this._getCategoryOptions(); 83 | if (options == null) return [] 84 | else return options(); 85 | } 86 | 87 | return this.props.options; 88 | }, 89 | 90 | _getHeader: function() { 91 | if (this.state.category=="") { 92 | return "Category"; 93 | } else if (this.state.operator=="") { 94 | return "Operator"; 95 | } else { 96 | return "Value"; 97 | } 98 | 99 | return this.props.options; 100 | }, 101 | 102 | _getCategoryType: function() { 103 | for (var i = 0; i < this.props.options.length; i++) { 104 | if (this.props.options[i].category == this.state.category) { 105 | categoryType = this.props.options[i].type; 106 | return categoryType; 107 | } 108 | } 109 | }, 110 | 111 | _getCategoryOptions: function() { 112 | for (var i = 0; i < this.props.options.length; i++) { 113 | if (this.props.options[i].category == this.state.category) { 114 | return this.props.options[i].options; 115 | } 116 | } 117 | }, 118 | 119 | 120 | _onKeyDown: function(event) { 121 | // We only care about intercepting backspaces 122 | if (event.keyCode !== KeyEvent.DOM_VK_BACK_SPACE) { 123 | return; 124 | } 125 | 126 | // Remove token ONLY when bksp pressed at beginning of line 127 | // without a selection 128 | var entry = this.refs.typeahead.inputRef().getDOMNode(); 129 | if (entry.selectionStart == entry.selectionEnd && 130 | entry.selectionStart == 0) 131 | { 132 | if (this.state.operator != "") { 133 | this.setState({operator: ""}); 134 | } else if (this.state.category != "") { 135 | this.setState({category: ""}); 136 | } else { 137 | // No tokens 138 | if (!this.state.selected.length) { 139 | return; 140 | } 141 | this._removeTokenForValue( 142 | this.state.selected[this.state.selected.length - 1] 143 | ); 144 | } 145 | event.preventDefault(); 146 | } 147 | }, 148 | 149 | _removeTokenForValue: function(value) { 150 | var index = this.state.selected.indexOf(value); 151 | if (index == -1) { 152 | return; 153 | } 154 | 155 | this.state.selected.splice(index, 1); 156 | this.setState({selected: this.state.selected}); 157 | this.props.onTokenRemove(this.state.selected); 158 | 159 | return; 160 | }, 161 | 162 | _addTokenForValue: function(value) { 163 | if (this.state.category == "") { 164 | this.setState({category: value}); 165 | this.refs.typeahead.setEntryText(""); 166 | return; 167 | } 168 | 169 | if (this.state.operator == "") { 170 | this.setState({operator: value}); 171 | this.refs.typeahead.setEntryText(""); 172 | return; 173 | } 174 | 175 | value = {"category":this.state.category,"operator":this.state.operator,"value":value}; 176 | 177 | this.state.selected.push(value); 178 | this.setState({selected: this.state.selected}); 179 | this.refs.typeahead.setEntryText(""); 180 | this.props.onTokenAdd(this.state.selected); 181 | 182 | this.setState({category: "", operator: ""}); 183 | 184 | return; 185 | }, 186 | 187 | /*** 188 | * Returns the data type the input should use ("date" or "text") 189 | */ 190 | _getInputType: function() { 191 | if (this.state.category != "" && this.state.operator != "") { 192 | return this._getCategoryType(); 193 | } else { 194 | return "text"; 195 | } 196 | }, 197 | 198 | render: function() { 199 | var classes = {} 200 | classes[this.props.customClasses.typeahead] = !!this.props.customClasses.typeahead; 201 | var classList = React.addons.classSet(classes); 202 | return ( 203 |
204 | 205 | 206 | 207 |
208 | { this._renderTokens() } 209 | 210 |
211 |
{ this.state.category }
212 |
{ this.state.operator }
213 | 214 | 224 |
225 |
226 |
227 | ) 228 | } 229 | }); 230 | 231 | module.exports = TypeaheadTokenizer; 232 | -------------------------------------------------------------------------------- /src/react-typeahead/tokenizer/token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | var React = window.React || require('react'); 6 | 7 | /** 8 | * Encapsulates the rendering of an option that has been "selected" in a 9 | * TypeaheadTokenizer 10 | */ 11 | var Token = React.createClass({ 12 | propTypes: { 13 | children: React.PropTypes.object, 14 | onRemove: React.PropTypes.func 15 | }, 16 | 17 | render: function() { 18 | return ( 19 |
20 | {this.props.children["category"]} {this.props.children["operator"]} "{this.props.children["value"]}" 21 | {this._makeCloseButton()} 22 |
23 | ); 24 | }, 25 | 26 | _makeCloseButton: function() { 27 | if (!this.props.onRemove) { 28 | return ""; 29 | } 30 | return ( 31 | × 35 | ); 36 | } 37 | }); 38 | 39 | module.exports = Token; 40 | -------------------------------------------------------------------------------- /src/react-typeahead/typeahead/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | var React = window.React || require('react/addons'); 6 | var TypeaheadSelector = require('./selector'); 7 | var KeyEvent = require('../keyevent'); 8 | var fuzzy = require('fuzzy'); 9 | var DatePicker = require('../../react-datepicker/datepicker.js'); 10 | var moment = require('moment'); 11 | 12 | /** 13 | * A "typeahead", an auto-completing text input 14 | * 15 | * Renders an text input that shows options nearby that you can use the 16 | * keyboard or mouse to select. Requires CSS for MASSIVE DAMAGE. 17 | */ 18 | var Typeahead = React.createClass({ 19 | propTypes: { 20 | customClasses: React.PropTypes.object, 21 | maxVisible: React.PropTypes.number, 22 | options: React.PropTypes.array, 23 | header: React.PropTypes.string, 24 | datatype: React.PropTypes.string, 25 | defaultValue: React.PropTypes.string, 26 | placeholder: React.PropTypes.string, 27 | onOptionSelected: React.PropTypes.func, 28 | onKeyDown: React.PropTypes.func 29 | }, 30 | 31 | mixins: [ 32 | require('react-onclickoutside') 33 | ], 34 | 35 | getDefaultProps: function() { 36 | return { 37 | options: [], 38 | header: "Category", 39 | datatype: "text", 40 | customClasses: {}, 41 | defaultValue: "", 42 | placeholder: "", 43 | onKeyDown: function(event) { return }, 44 | onOptionSelected: function(option) { } 45 | }; 46 | }, 47 | 48 | getInitialState: function() { 49 | return { 50 | // The set of all options... Does this need to be state? I guess for lazy load... 51 | options: this.props.options, 52 | header: this.props.header, 53 | datatype: this.props.datatype, 54 | 55 | focused: false, 56 | 57 | // The currently visible set of options 58 | visible: this.getOptionsForValue(this.props.defaultValue, this.props.options), 59 | 60 | // This should be called something else, "entryValue" 61 | entryValue: this.props.defaultValue, 62 | 63 | // A valid typeahead value 64 | selection: null 65 | }; 66 | }, 67 | 68 | componentWillReceiveProps: function(nextProps) { 69 | this.setState({options: nextProps.options, 70 | header: nextProps.header, 71 | datatype: nextProps.datatype, 72 | visible: nextProps.options}); 73 | }, 74 | 75 | getOptionsForValue: function(value, options) { 76 | var result = fuzzy.filter(value, options).map(function(res) { 77 | return res.string; 78 | }); 79 | 80 | if (this.props.maxVisible) { 81 | result = result.slice(0, this.props.maxVisible); 82 | } 83 | return result; 84 | }, 85 | 86 | setEntryText: function(value) { 87 | if (this.refs.entry != null) { 88 | this.refs.entry.getDOMNode().value = value; 89 | } 90 | this._onTextEntryUpdated(); 91 | }, 92 | 93 | _renderIncrementalSearchResults: function() { 94 | if (!this.state.focused) { 95 | return ""; 96 | } 97 | 98 | // Something was just selected 99 | if (this.state.selection) { 100 | return ""; 101 | } 102 | 103 | // There are no typeahead / autocomplete suggestions 104 | if (!this.state.visible.length) { 105 | return ""; 106 | } 107 | 108 | return ( 109 | 113 | ); 114 | }, 115 | 116 | _onOptionSelected: function(option) { 117 | var nEntry = this.refs.entry.getDOMNode(); 118 | nEntry.focus(); 119 | nEntry.value = option; 120 | this.setState({visible: this.getOptionsForValue(option, this.state.options), 121 | selection: option, 122 | entryValue: option}); 123 | 124 | this.props.onOptionSelected(option); 125 | }, 126 | 127 | _onTextEntryUpdated: function() { 128 | var value = ""; 129 | if (this.refs.entry != null) { 130 | value = this.refs.entry.getDOMNode().value; 131 | } 132 | this.setState({visible: this.getOptionsForValue(value, this.state.options), 133 | selection: null, 134 | entryValue: value}); 135 | }, 136 | 137 | _onEnter: function(event) { 138 | if (!this.refs.sel.state.selection) { 139 | return this.props.onKeyDown(event); 140 | } 141 | 142 | this._onOptionSelected(this.refs.sel.state.selection); 143 | }, 144 | 145 | _onEscape: function() { 146 | this.refs.sel.setSelectionIndex(null) 147 | }, 148 | 149 | _onTab: function(event) { 150 | var option = this.refs.sel.state.selection ? 151 | this.refs.sel.state.selection : this.state.visible[0]; 152 | this._onOptionSelected(option) 153 | }, 154 | 155 | eventMap: function(event) { 156 | var events = {}; 157 | 158 | events[KeyEvent.DOM_VK_UP] = this.refs.sel.navUp; 159 | events[KeyEvent.DOM_VK_DOWN] = this.refs.sel.navDown; 160 | events[KeyEvent.DOM_VK_RETURN] = events[KeyEvent.DOM_VK_ENTER] = this._onEnter; 161 | events[KeyEvent.DOM_VK_ESCAPE] = this._onEscape; 162 | events[KeyEvent.DOM_VK_TAB] = this._onTab; 163 | 164 | return events; 165 | }, 166 | 167 | _onKeyDown: function(event) { 168 | // If Enter pressed 169 | if (event.keyCode === KeyEvent.DOM_VK_RETURN || event.keyCode === KeyEvent.DOM_VK_ENTER) { 170 | // If no options were provided so we can match on anything 171 | if (this.props.options.length===0) { 172 | this._onOptionSelected(this.state.entryValue); 173 | } 174 | 175 | // If what has been typed in is an exact match of one of the options 176 | if (this.props.options.indexOf(this.state.entryValue) > -1) { 177 | this._onOptionSelected(this.state.entryValue); 178 | } 179 | } 180 | 181 | // If there are no visible elements, don't perform selector navigation. 182 | // Just pass this up to the upstream onKeydown handler 183 | if (!this.refs.sel) { 184 | return this.props.onKeyDown(event); 185 | } 186 | 187 | var handler = this.eventMap()[event.keyCode]; 188 | 189 | if (handler) { 190 | handler(event); 191 | } else { 192 | return this.props.onKeyDown(event); 193 | } 194 | // Don't propagate the keystroke back to the DOM/browser 195 | event.preventDefault(); 196 | }, 197 | 198 | _onFocus: function(event) { 199 | this.setState({focused: true}); 200 | }, 201 | 202 | handleClickOutside: function(event) { 203 | this.setState({focused:false}); 204 | }, 205 | 206 | isDescendant: function(parent, child) { 207 | var node = child.parentNode; 208 | while (node != null) { 209 | if (node == parent) { 210 | return true; 211 | } 212 | node = node.parentNode; 213 | } 214 | return false; 215 | }, 216 | 217 | _handleDateChange: function(date) { 218 | this.props.onOptionSelected(date.format("YYYY-MM-DD")); 219 | }, 220 | 221 | _showDatePicker: function() { 222 | if (this.state.datatype == "date") { 223 | return true; 224 | } 225 | return false; 226 | }, 227 | 228 | inputRef: function() { 229 | if (this._showDatePicker()) { 230 | return this.refs.datepicker.refs.dateinput.refs.entry; 231 | } else { 232 | return this.refs.entry; 233 | } 234 | }, 235 | 236 | render: function() { 237 | var inputClasses = {} 238 | inputClasses[this.props.customClasses.input] = !!this.props.customClasses.input; 239 | var inputClassList = React.addons.classSet(inputClasses) 240 | 241 | var classes = { 242 | typeahead: true 243 | } 244 | classes[this.props.className] = !!this.props.className; 245 | var classList = React.addons.classSet(classes); 246 | 247 | if (this._showDatePicker()) { 248 | return ( 249 | 250 | 251 | 252 | ); 253 | } 254 | 255 | return ( 256 | 257 | 262 | { this._renderIncrementalSearchResults() } 263 | 264 | ); 265 | } 266 | }); 267 | 268 | module.exports = Typeahead; 269 | -------------------------------------------------------------------------------- /src/react-typeahead/typeahead/option.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | var React = window.React || require('react/addons'); 6 | 7 | /** 8 | * A single option within the TypeaheadSelector 9 | */ 10 | var TypeaheadOption = React.createClass({ 11 | propTypes: { 12 | customClasses: React.PropTypes.object, 13 | onClick: React.PropTypes.func, 14 | children: React.PropTypes.string 15 | }, 16 | 17 | getDefaultProps: function() { 18 | return { 19 | customClasses: {}, 20 | onClick: function(event) { 21 | event.preventDefault(); 22 | } 23 | }; 24 | }, 25 | 26 | getInitialState: function() { 27 | return { 28 | hover: false 29 | }; 30 | }, 31 | 32 | render: function() { 33 | var classes = { 34 | hover: this.props.hover 35 | } 36 | classes[this.props.customClasses.listItem] = !!this.props.customClasses.listItem; 37 | var classList = React.addons.classSet(classes); 38 | 39 | return ( 40 |
  • 41 | 42 | { this.props.children } 43 | 44 |
  • 45 | ); 46 | }, 47 | 48 | _getClasses: function() { 49 | var classes = { 50 | "typeahead-option": true, 51 | }; 52 | classes[this.props.customClasses.listAnchor] = !!this.props.customClasses.listAnchor; 53 | return React.addons.classSet(classes); 54 | }, 55 | 56 | _onClick: function() { 57 | return this.props.onClick(); 58 | } 59 | }); 60 | 61 | 62 | module.exports = TypeaheadOption; 63 | -------------------------------------------------------------------------------- /src/react-typeahead/typeahead/selector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | var React = window.React || require('react/addons'); 6 | var TypeaheadOption = require('./option'); 7 | 8 | /** 9 | * Container for the options rendered as part of the autocompletion process 10 | * of the typeahead 11 | */ 12 | var TypeaheadSelector = React.createClass({ 13 | propTypes: { 14 | options: React.PropTypes.array, 15 | header: React.PropTypes.string, 16 | customClasses: React.PropTypes.object, 17 | selectionIndex: React.PropTypes.number, 18 | onOptionSelected: React.PropTypes.func 19 | }, 20 | 21 | getDefaultProps: function() { 22 | return { 23 | selectionIndex: null, 24 | customClasses: {}, 25 | onOptionSelected: function(option) { } 26 | }; 27 | }, 28 | 29 | getInitialState: function() { 30 | return { 31 | selectionIndex: this.props.selectionIndex, 32 | selection: this.getSelectionForIndex(this.props.selectionIndex) 33 | }; 34 | }, 35 | 36 | componentWillReceiveProps: function(nextProps) { 37 | this.setState({selectionIndex: null}); 38 | }, 39 | 40 | render: function() { 41 | var classes = { 42 | "typeahead-selector": true 43 | }; 44 | classes[this.props.customClasses.results] = this.props.customClasses.results; 45 | var classList = React.addons.classSet(classes); 46 | 47 | var results = this.props.options.map(function(result, i) { 48 | return ( 49 | 53 | { result } 54 | 55 | ); 56 | }, this); 57 | return
      58 |
    • {this.props.header}
    • 59 | { results } 60 |
    ; 61 | }, 62 | 63 | setSelectionIndex: function(index) { 64 | this.setState({ 65 | selectionIndex: index, 66 | selection: this.getSelectionForIndex(index), 67 | }); 68 | }, 69 | 70 | getSelectionForIndex: function(index) { 71 | if (index === null) { 72 | return null; 73 | } 74 | return this.props.options[index]; 75 | }, 76 | 77 | _onClick: function(result) { 78 | this.props.onOptionSelected(result); 79 | }, 80 | 81 | _nav: function(delta) { 82 | if (!this.props.options) { 83 | return; 84 | } 85 | var newIndex; 86 | if (this.state.selectionIndex === null) { 87 | if (delta == 1) { 88 | newIndex = 0; 89 | } else { 90 | newIndex = delta; 91 | } 92 | } else { 93 | newIndex = this.state.selectionIndex + delta; 94 | } 95 | if (newIndex < 0) { 96 | newIndex += this.props.options.length; 97 | } else if (newIndex >= this.props.options.length) { 98 | newIndex -= this.props.options.length; 99 | } 100 | var newSelection = this.getSelectionForIndex(newIndex); 101 | this.setState({selectionIndex: newIndex, 102 | selection: newSelection}); 103 | }, 104 | 105 | navDown: function() { 106 | this._nav(1); 107 | }, 108 | 109 | navUp: function() { 110 | this._nav(-1); 111 | } 112 | 113 | }); 114 | 115 | module.exports = TypeaheadSelector; 116 | --------------------------------------------------------------------------------