├── .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 |
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 |
--------------------------------------------------------------------------------