├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── css
├── _react-virtualized-styles.scss
└── style.scss
├── dist
├── propersearch.css
├── propersearch.js
├── propersearch.min.css
└── propersearch.min.js
├── examples
├── dist
│ └── app.js
├── favicon.ico
├── index.html
├── jsx
│ ├── app.js
│ └── example.js
└── screenshots
│ └── example.png
├── index.html
├── karma.conf.js
├── lib
├── components
│ ├── __tests__
│ │ ├── search-test.js
│ │ └── searchList-test.js
│ ├── search.js
│ └── searchList.js
├── lang
│ └── messages.js
├── lib
│ └── cache.js
├── propersearch.js
└── utils
│ └── normalize.js
├── package.json
├── src
├── css
│ ├── _react-virtualized-styles.scss
│ └── style.scss
└── jsx
│ ├── components
│ ├── __tests__
│ │ ├── search-test.js
│ │ └── searchList-test.js
│ ├── search.js
│ └── searchList.js
│ ├── lang
│ └── messages.js
│ ├── lib
│ └── cache.js
│ ├── propersearch.js
│ └── utils
│ └── normalize.js
├── tests.webpack.js
├── webpack.config.example.dist.js
├── webpack.config.example.js
├── webpack.config.js
└── webpack.config.min.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | "es2015"
5 | ],
6 | "plugins": [
7 | "add-module-exports",
8 | "transform-es3-member-expression-literals",
9 | "transform-es3-property-literals"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "plugins": [
4 | "react"
5 | ],
6 | "ecmaFeatures": {
7 | "jsx": true,
8 | "modules": true
9 | },
10 | "env": {
11 | "browser": true,
12 | "node": true,
13 | "es6": true
14 | },
15 | "rules": {
16 | "strict": 0
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | webpack.config.example.js
3 | webpack.config.js
4 | webpack.config.min.js
5 | examples/*
6 | dist/*.html
7 | dist/*.js
8 | src/**/__tests__/
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4.4"
4 | install: npm install
5 | before_install:
6 | - npm -g install karma-cli
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 CBI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ProperSearch
2 |
3 | [](https://travis-ci.org/CBIConsulting/ProperSearch)
4 |
5 | A proper search component for react. With a search field and a list of items allows the user to filter that list and select the items. The component return the
6 | selected data when it get selected. Allows multi and single selection. The list is virtual rendered, was designed to handle thousands of items without sacrificing
7 | performance, only render the items in the view. Use react-virtualized to render the list items. This component has a lot of configurable settings, read the
8 | component properties section for more info.
9 |
10 | Used technologies:
11 |
12 | - React
13 | - ES6
14 | - Webpack
15 | - Babel
16 | - Node
17 | - Compass
18 | - Jasmine
19 | - Karma
20 |
21 |
22 |
23 | Features of ProperSearch:
24 |
25 | * Data selection allowed from a list
26 | * List filtering on search
27 | * Allow multi and single selection
28 | * Return the selection
29 | * List virtual rendered
30 |
31 |
32 | The compile and compressed ProperSearch distribution file can be found in the dist folder along with the css file. Add the default stylesheet `dist/propersearch.min.css`, then import it into any module.
33 |
34 | ## Live Demo
35 | ##### [Code](https://github.com/CBIConsulting/ProperSearch/tree/gh-pages/examples/jsx/app.js)
36 | ##### [Demo](http://cbiconsulting.github.io/ProperSearch/)
37 |
38 |
39 | ## External dependencies
40 | * React and React DOM
41 | * Underscore
42 |
43 |
44 | ## Preview
45 | 
46 |
47 | ## Use this module in your projects
48 | ```
49 | npm install react-propersearch --save
50 | ```
51 |
52 | ## How to start
53 |
54 | Run:
55 | ```
56 | npm install
57 | npm start
58 | ```
59 |
60 | Check your http://localhost:8080/ or `open http://localhost:8080/`
61 |
62 | ## How to test
63 |
64 | `npm test`
65 |
66 | ### Component properties
67 | * data: List data. (Array) (You can send data as an Inmutable but it should have a similar structure to the procesed data in method prepareData() [src/search::445](https://github.com/CBIConsulting/ProperSearch/tree/dev/src/jsx/components/search.js), and in this case don't forget to send indexed and rawdata too. It's not recomended)
68 | * value: Id field name. (String)
69 | * label: Name of the field to be displayed (String)
70 | * messages: Get the translated messages of the lang selected in the property lang. Default ENG (An example can be found in src/lang)
71 | * Default:
72 | ```javascript
73 | 'ENG': {
74 | all: 'Select All',
75 | none: 'Unselect All',
76 | loading: 'Loading...',
77 | noData:'No data found'
78 | }
79 | ```
80 | * lang: Language for the component (String)
81 | * allowsEmptySelection: The empty string values will never be rendered into the list but if you set this prop to true then a new button will appear. When you click that button ('Select Empty') you'll get selection => [''] and the data array with all the elements that has empty values in idField or displayField
82 | * rowFormater: Process the data of each element of the list, it's a function that get the value, it should return the value formated. (NOTE: If the element it's a function then this prop does nothing)
83 | * defaultSelection: Items of the list selected by default. (String or Array)
84 | * multiSelect: Type of the selection, multiple or single (Boolean)
85 | * listWidth: Custom width for the list under the search field (Integer) Default component's width.
86 | * listHeight: Height of the list. Default 200 (Integer)
87 | * listRowHeight: Height of each row of the list
88 | * afterSelect: Function called after select a row. Return the seleted rows.
89 | * Ex:
90 | ```javascript
91 | afterSelect ={
92 | function(data, selection){
93 | console.info(data); // Selected data
94 | console.info(selection); // Array of selected values
95 | }
96 | }
97 | ```
98 | * afterSearch: Function called after type something into the search field. Return the written string.
99 | * Ex:
100 | ```javascript
101 | afterSearch={
102 | function(search_string) {
103 | console.info('Filtering by: ', search_string);
104 | }
105 | }
106 | ```
107 | * afterSelectGetSelection: Function called after select a row. This one works same as afterSelec(data, selection) but is faster because doesn't work over data, only get selection instead. If you are working with large amount of data and just need the id's this one is more optimal.
108 | * Ex:
109 | ```javascript
110 | afterSelectGetSelection={
111 | function(selectionAsArray, selectionAsSetObj) {
112 | console.info(selectionAsArray); // Array of selected values (idField)
113 | console.info(selectionAsSetObj); // Set object which contains selected values (idField)
114 | }
115 | }
116 | ```
117 | * fieldClass: ClassName for the search field (String)
118 | * listClass: ClassName for the list (String)
119 | * listElementClass: ClassName for each element of the list (String)
120 | * className: ClassName for the component container (String)
121 | * placeholder: Placeholder for the search field (String) Default 'Search...'
122 | * searchIcon: ClassName for the search icon in the left of the search field (String) Default 'fa fa-search fa-fw' (FontAwesome)
123 | * clearIcon: ClassName for the clear icon (X) in the right side of the search field. (String) Default 'fa fa-times fa-fw' (FontAwesome)
124 | * throttle: Time between filtering action and the next. It affects to the search input onChange method setting an timeout (Integer) Default 160
125 | * minLength: Min. length of the search input to start filtering. (Integer) Default 3
126 | * onEnter: Custom function to be called on Enter key up.
127 | * idField: Name of the field that will be used to build the selection. Default 'value'
128 | * Ex:
129 | ```javascript
130 | let data = [];
131 | data.push(value:'3', label: 'Orange', price: '9', kg: 200);
132 |
133 |
138 |
139 | Selecting Orange you ill get a selection -> [3] and data -> [{value:'3', label: 'Orange', price: '9', kg: 200}]
140 | ```
141 | * displayField: Field of the data which should be used to display each element of the list. It can be a string or a function, just remenber to set the showIcon property to false if you are using another component and then only that component will be rendered inside each list element. Default: 'label'.
142 | * Ex:
143 | ```javascript
144 | let buttonClick = (e, name) => {
145 | alert('Button ' + name + ' has been clicked');
146 | }
147 |
148 | let formater = listElement => {
149 | return {buttonClick(e, listElement.name)} }>{ listElement.name } ;
150 | }
151 |
152 | let data = [];
153 | data.push(id:'16', display: formater, name: 'test 1');
154 |
155 |
161 | ```
162 | * listShowIcon: If the checked icon on the left of each list element must be printed or not
163 | * autoComplete: If the search field has autocomplete 'on' or 'off'. Default 'off'
164 | * defaultSearch: Set a default searching string to search input when the components get mounted or this prop is updated.
165 | * indexed: In case you want to use your own data (it has to be an Immutable obj) you must send indexed data by its idField.
166 | * rawdata: In case you want to use your own data (it has to be an Immutable obj) you must send raw data. (the data you'll get when someone clicks in list)
167 | * filterField: Field used for filtering on search input change.
168 | * filter: Function to filter on type something in the search input. By default the data will be filtered by its displayfield, if displayfield is a function then by it's name, if name doesn't exist then by its idField. (Important: If you set filterField then the data will be filter by the field you have chosen). Note: if you use the filter then you'll get each element of list and the search input value, then you can filter that data in the way you wanted). The search value it's normalized.
169 | * Ex:
170 | ```javascript
171 | let filter = (listElement, searchValue) => {
172 | let data = listElement.name.toLowerCase();
173 | data = Normalizer.normalize(data);
174 | return data.indexOf(searchValue) >= 0;
175 | }
176 |
177 | let buttonClick = (e, name) => {
178 | alert('Button ' + name + ' has been clicked');
179 | }
180 |
181 | let formater = listElement => {
182 | return {buttonClick(e, listElement.name)} }>{ listElement.name } ;
183 | }
184 |
185 | let data = [];
186 | data.push(id:'16', display: formater, name: 'test 1');
187 |
188 |
195 | * hiddenSelection: Thats the selection of what elements should not be displayed. It's an array, string, number or Set obj of the data ids (idField).
196 |
197 | ```
198 | ### Basic Example
199 |
200 | ```javascript
201 | import React from 'react';
202 | import ReactDOM from 'react-dom';
203 | import ProperSearch from 'react-propercombo';
204 |
205 | // Function Called after select items in the list.
206 |
207 | const afterSelect = (data, selection) => {
208 | console.info(data);
209 | console.info(selection);
210 | }
211 |
212 | // List data
213 | const data = [];
214 |
215 | for (var i = 10000; i >= 0; i--) {
216 | data.push({value: 'item-' + i, label: 'Item ' + i});
217 | }
218 |
219 | // Render the Search component
220 | ReactDOM.render(
221 | ,
226 | document.getElementById('example')
227 | );
228 | ```
229 |
230 |
231 | Contributions
232 | ------------
233 |
234 | Use [GitHub issues](https://github.com/CBIConsulting/ProperSearch/issues) for requests.
235 |
236 | Changelog
237 | ---------
238 |
239 | Changes are tracked as [GitHub releases](https://github.com/CBIConsulting/ProperSearch/releases).
240 |
--------------------------------------------------------------------------------
/css/_react-virtualized-styles.scss:
--------------------------------------------------------------------------------
1 | /* Grid default theme */
2 |
3 | .Grid {
4 | position: relative;
5 | overflow: auto;
6 | -webkit-overflow-scrolling: touch;
7 |
8 | /* Without this property, Chrome repaints the entire Grid any time a new row or column is added.
9 | Firefox only repaints the new row or column (regardless of this property).
10 | Safari and IE don't support the property at all. */
11 | will-change: transform;
12 | }
13 |
14 | .Grid__innerScrollContainer {
15 | box-sizing: border-box;
16 | overflow: hidden;
17 | }
18 |
19 | .Grid__cell {
20 | position: absolute;
21 | }
22 |
23 | /* FlexTable default theme */
24 |
25 | .FlexTable {
26 | }
27 |
28 | .FlexTable__Grid {
29 | overflow-x: hidden;
30 | }
31 |
32 | .FlexTable__headerRow {
33 | font-weight: 700;
34 | text-transform: uppercase;
35 | display: flex;
36 | flex-direction: row;
37 | align-items: center;
38 | overflow: hidden;
39 | }
40 | .FlexTable__headerTruncatedText {
41 | white-space: nowrap;
42 | text-overflow: ellipsis;
43 | overflow: hidden;
44 | }
45 | .FlexTable__row {
46 | display: flex;
47 | flex-direction: row;
48 | align-items: center;
49 | overflow: hidden;
50 | }
51 |
52 | .FlexTable__headerColumn,
53 | .FlexTable__rowColumn {
54 | margin-right: 10px;
55 | min-width: 0px;
56 | }
57 |
58 | .FlexTable__headerColumn:first-of-type,
59 | .FlexTable__rowColumn:first-of-type {
60 | margin-left: 10px;
61 | }
62 | .FlexTable__headerColumn {
63 | align-items: center;
64 | display: flex;
65 | flex-direction: row;
66 | overflow: hidden;
67 | }
68 | .FlexTable__sortableHeaderColumn {
69 | cursor: pointer;
70 | }
71 | .FlexTable__rowColumn {
72 | justify-content: center;
73 | flex-direction: column;
74 | display: flex;
75 | overflow: hidden;
76 | height: 100%;
77 | }
78 |
79 | .FlexTable__sortableHeaderIconContainer {
80 | display: flex;
81 | align-items: center;
82 | }
83 | .FlexTable__sortableHeaderIcon {
84 | flex: 0 0 24px;
85 | height: 1em;
86 | width: 1em;
87 | fill: currentColor;
88 | }
89 |
90 | .FlexTable__truncatedColumnText {
91 | text-overflow: ellipsis;
92 | overflow: hidden;
93 | }
94 |
95 | /* VirtualScroll default theme */
96 |
97 | .VirtualScroll {
98 | position: relative;
99 | overflow-y: auto;
100 | overflow-x: hidden;
101 | -webkit-overflow-scrolling: touch;
102 | }
103 |
104 | .VirtualScroll__innerScrollContainer {
105 | box-sizing: border-box;
106 | overflow: hidden;
107 | }
108 |
109 | .VirtualScroll__row {
110 | position: absolute;
111 | }
--------------------------------------------------------------------------------
/css/style.scss:
--------------------------------------------------------------------------------
1 | @import "compass";
2 | @import "react-virtualized-styles";
3 |
4 | .proper-search {
5 | background-color: #fff;
6 |
7 | .proper-search-field {
8 | font-size: 14px;
9 | margin: 0;
10 | line-height: 22px;
11 | border: none;
12 |
13 | .proper-search-input {
14 | background: #fff;
15 | padding: 3px 2px;
16 | background-color: #ccc;
17 | @include border-radius(3px 2px, 2px 3px);
18 |
19 | input[type="text"] {
20 | width: 100%;
21 | height: 1.8em;
22 | margin-bottom: 0;
23 | color: #777777;
24 | padding-left: 30px;
25 | padding-right: 30px;
26 | border: none;
27 | outline: none;
28 | box-shadow: none;
29 | webkit-box-shadow: none;
30 | }
31 |
32 | i.proper-search-field-icon {
33 | color: #777777;
34 | overflow: visible;
35 | margin-right: -20px;
36 | position: absolute;
37 | border: 0;
38 | padding: 4px;
39 | }
40 |
41 | .btn-clear {
42 | i {
43 | color: #777777;
44 | }
45 | background: none;
46 | overflow: visible;
47 | position: absolute;
48 | border: 0;
49 | padding-top: 0.2em;
50 | margin-left: -26px;
51 | outline: none;
52 |
53 | &:active, &:focus {
54 | box-shadow: none;
55 | webkit-box-shadow: none;
56 | }
57 | }
58 | }
59 | }
60 |
61 | .proper-search-list {
62 | .proper-search-list-bar {
63 | background-color: #f6f7f8;
64 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjEuMCIgeDI9IjAuNSIgeTI9IjAuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
65 | background-size: 100%;
66 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, rgba(230, 230, 230, 0.61)), color-stop(100%, #ffffff));
67 | background-image: -moz-linear-gradient(bottom, rgba(230,230,230,0.61), #ffffff);
68 | background-image: -webkit-linear-gradient(#fff,#efefef);
69 | background-image: linear-gradient(#fff,#efefef);
70 | padding: 1px 0;
71 | width: 100%;
72 | border: 0;
73 | line-height: 5px;
74 |
75 | .btn-group {
76 | width: 100%;
77 | }
78 |
79 | .btn-group label {
80 | position: relative;
81 | cursor: pointer;
82 | margin: 0;
83 | font-weight: 100;
84 | font-family: Helvetica;
85 | font-size: x-small;
86 | }
87 |
88 | .btn-group .btn-select {
89 | display: inline-block;
90 | line-height: 1.4;
91 | text-align: center;
92 | width: 100%;
93 | cursor: pointer !important;
94 | border: none;
95 | margin: 0;
96 | padding: 5px 8px 5px 8px !important;
97 | border-radius: 0;
98 | color: #333;
99 | }
100 |
101 | .btn-group .btn-select:hover,
102 | .btn-group .btn-select:focus,
103 | .btn-group .btn-select:active,
104 | .btn-group .btn-select.disabled,
105 | .btn-group .btn-select[disabled] {
106 | background-color: #CECECE;
107 | text-decoration: none;
108 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #CECECE), color-stop(100%, #ffffff));
109 | background-image: -moz-linear-gradient(bottom, #CECECE, #ffffff);
110 | background-image: -webkit-linear-gradient(#fff,#CECECE);
111 | background-image: linear-gradient(#fff,#CECECE);
112 | }
113 | }
114 |
115 | .proper-search-list-virtual {
116 | &:focus,&:active {
117 | border: none;
118 | outline: none;
119 | }
120 |
121 | .proper-search-list-element {
122 | height: inherit;
123 | width: inherit;
124 | padding-left: 0.3em;
125 | margin-bottom: 0.1em;
126 | padding-top: 0.1em;
127 | cursor: pointer;
128 | white-space: nowrap;
129 | overflow: hidden;
130 | text-overflow: ellipsis;
131 | box-sizing: border-box;
132 |
133 | &.proper-search-selected {
134 | i {
135 | color: #10AB10;
136 | }
137 | }
138 |
139 | &.proper-search-single-selected {
140 | background-color: #C7EBFF;
141 | }
142 |
143 | i {
144 | width: 14.5px;
145 | margin-right: 8px;
146 | }
147 | }
148 | }
149 | }
150 |
151 | .proper-search-loading {
152 | text-align: center;
153 | font-size: 14px;
154 | margin: auto;
155 | }
156 | }
--------------------------------------------------------------------------------
/dist/propersearch.css:
--------------------------------------------------------------------------------
1 | /* Grid default theme */
2 | .Grid {
3 | position: relative;
4 | overflow: auto;
5 | -webkit-overflow-scrolling: touch;
6 | /* Without this property, Chrome repaints the entire Grid any time a new row or column is added.
7 | Firefox only repaints the new row or column (regardless of this property).
8 | Safari and IE don't support the property at all. */
9 | will-change: transform; }
10 |
11 | .Grid__innerScrollContainer {
12 | box-sizing: border-box;
13 | overflow: hidden; }
14 |
15 | .Grid__cell {
16 | position: absolute; }
17 |
18 | /* FlexTable default theme */
19 | .FlexTable__Grid {
20 | overflow-x: hidden; }
21 |
22 | .FlexTable__headerRow {
23 | font-weight: 700;
24 | text-transform: uppercase;
25 | display: flex;
26 | flex-direction: row;
27 | align-items: center;
28 | overflow: hidden; }
29 |
30 | .FlexTable__headerTruncatedText {
31 | white-space: nowrap;
32 | text-overflow: ellipsis;
33 | overflow: hidden; }
34 |
35 | .FlexTable__row {
36 | display: flex;
37 | flex-direction: row;
38 | align-items: center;
39 | overflow: hidden; }
40 |
41 | .FlexTable__headerColumn,
42 | .FlexTable__rowColumn {
43 | margin-right: 10px;
44 | min-width: 0px; }
45 |
46 | .FlexTable__headerColumn:first-of-type,
47 | .FlexTable__rowColumn:first-of-type {
48 | margin-left: 10px; }
49 |
50 | .FlexTable__headerColumn {
51 | align-items: center;
52 | display: flex;
53 | flex-direction: row;
54 | overflow: hidden; }
55 |
56 | .FlexTable__sortableHeaderColumn {
57 | cursor: pointer; }
58 |
59 | .FlexTable__rowColumn {
60 | justify-content: center;
61 | flex-direction: column;
62 | display: flex;
63 | overflow: hidden;
64 | height: 100%; }
65 |
66 | .FlexTable__sortableHeaderIconContainer {
67 | display: flex;
68 | align-items: center; }
69 |
70 | .FlexTable__sortableHeaderIcon {
71 | flex: 0 0 24px;
72 | height: 1em;
73 | width: 1em;
74 | fill: currentColor; }
75 |
76 | .FlexTable__truncatedColumnText {
77 | text-overflow: ellipsis;
78 | overflow: hidden; }
79 |
80 | /* VirtualScroll default theme */
81 | .VirtualScroll {
82 | position: relative;
83 | overflow-y: auto;
84 | overflow-x: hidden;
85 | -webkit-overflow-scrolling: touch; }
86 |
87 | .VirtualScroll__innerScrollContainer {
88 | box-sizing: border-box;
89 | overflow: hidden; }
90 |
91 | .VirtualScroll__row {
92 | position: absolute; }
93 |
94 | .proper-search {
95 | background-color: #fff; }
96 | .proper-search .proper-search-field {
97 | font-size: 14px;
98 | margin: 0;
99 | line-height: 22px;
100 | border: none; }
101 | .proper-search .proper-search-field .proper-search-input {
102 | background: #fff;
103 | padding: 3px 2px;
104 | background-color: #ccc;
105 | -webkit-border-radius: 3px 2px;
106 | -moz-border-radius: 3px 2px / 2px 3px;
107 | border-radius: 3px 2px / 2px 3px; }
108 | .proper-search .proper-search-field .proper-search-input input[type="text"] {
109 | width: 100%;
110 | height: 1.8em;
111 | margin-bottom: 0;
112 | color: #777777;
113 | padding-left: 30px;
114 | padding-right: 30px;
115 | border: none;
116 | outline: none;
117 | box-shadow: none;
118 | webkit-box-shadow: none; }
119 | .proper-search .proper-search-field .proper-search-input i.proper-search-field-icon {
120 | color: #777777;
121 | overflow: visible;
122 | margin-right: -20px;
123 | position: absolute;
124 | border: 0;
125 | padding: 4px; }
126 | .proper-search .proper-search-field .proper-search-input .btn-clear {
127 | background: none;
128 | overflow: visible;
129 | position: absolute;
130 | border: 0;
131 | padding-top: 0.2em;
132 | margin-left: -26px;
133 | outline: none; }
134 | .proper-search .proper-search-field .proper-search-input .btn-clear i {
135 | color: #777777; }
136 | .proper-search .proper-search-field .proper-search-input .btn-clear:active, .proper-search .proper-search-field .proper-search-input .btn-clear:focus {
137 | box-shadow: none;
138 | webkit-box-shadow: none; }
139 | .proper-search .proper-search-list .proper-search-list-bar {
140 | background-color: #f6f7f8;
141 | background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjEuMCIgeDI9IjAuNSIgeTI9IjAuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==");
142 | background-size: 100%;
143 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, rgba(230, 230, 230, 0.61)), color-stop(100%, #ffffff));
144 | background-image: -moz-linear-gradient(bottom, rgba(230, 230, 230, 0.61), #ffffff);
145 | background-image: -webkit-linear-gradient(#fff, #efefef);
146 | background-image: linear-gradient(#fff, #efefef);
147 | padding: 1px 0;
148 | width: 100%;
149 | border: 0;
150 | line-height: 5px; }
151 | .proper-search .proper-search-list .proper-search-list-bar .btn-group {
152 | width: 100%; }
153 | .proper-search .proper-search-list .proper-search-list-bar .btn-group label {
154 | position: relative;
155 | cursor: pointer;
156 | margin: 0;
157 | font-weight: 100;
158 | font-family: Helvetica;
159 | font-size: x-small; }
160 | .proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select {
161 | display: inline-block;
162 | line-height: 1.4;
163 | text-align: center;
164 | width: 100%;
165 | cursor: pointer !important;
166 | border: none;
167 | margin: 0;
168 | padding: 5px 8px 5px 8px !important;
169 | border-radius: 0;
170 | color: #333; }
171 | .proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select:hover,
172 | .proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select:focus,
173 | .proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select:active,
174 | .proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select.disabled,
175 | .proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select[disabled] {
176 | background-color: #CECECE;
177 | text-decoration: none;
178 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #CECECE), color-stop(100%, #ffffff));
179 | background-image: -moz-linear-gradient(bottom, #CECECE, #ffffff);
180 | background-image: -webkit-linear-gradient(#fff, #CECECE);
181 | background-image: linear-gradient(#fff, #CECECE); }
182 | .proper-search .proper-search-list .proper-search-list-virtual:focus, .proper-search .proper-search-list .proper-search-list-virtual:active {
183 | border: none;
184 | outline: none; }
185 | .proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element {
186 | height: inherit;
187 | width: inherit;
188 | padding-left: 0.3em;
189 | margin-bottom: 0.1em;
190 | padding-top: 0.1em;
191 | cursor: pointer;
192 | white-space: nowrap;
193 | overflow: hidden;
194 | text-overflow: ellipsis;
195 | box-sizing: border-box; }
196 | .proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element.proper-search-selected i {
197 | color: #10AB10; }
198 | .proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element.proper-search-single-selected {
199 | background-color: #C7EBFF; }
200 | .proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element i {
201 | width: 14.5px;
202 | margin-right: 8px; }
203 | .proper-search .proper-search-loading {
204 | text-align: center;
205 | font-size: 14px;
206 | margin: auto; }
207 |
--------------------------------------------------------------------------------
/dist/propersearch.min.css:
--------------------------------------------------------------------------------
1 | .Grid{position:relative;overflow:auto;-webkit-overflow-scrolling:touch;will-change:transform}.Grid__innerScrollContainer{box-sizing:border-box;overflow:hidden}.Grid__cell{position:absolute}.FlexTable__Grid{overflow-x:hidden}.FlexTable__headerRow{font-weight:700;text-transform:uppercase;display:flex;flex-direction:row;align-items:center;overflow:hidden}.FlexTable__headerTruncatedText{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.FlexTable__row{display:flex;flex-direction:row;align-items:center;overflow:hidden}.FlexTable__headerColumn,.FlexTable__rowColumn{margin-right:10px;min-width:0}.FlexTable__headerColumn:first-of-type,.FlexTable__rowColumn:first-of-type{margin-left:10px}.FlexTable__headerColumn{align-items:center;display:flex;flex-direction:row;overflow:hidden}.FlexTable__sortableHeaderColumn{cursor:pointer}.FlexTable__rowColumn{justify-content:center;flex-direction:column;display:flex;overflow:hidden;height:100%}.FlexTable__sortableHeaderIconContainer{display:flex;align-items:center}.FlexTable__sortableHeaderIcon{flex:0 0 24px;height:1em;width:1em;fill:currentColor}.FlexTable__truncatedColumnText{text-overflow:ellipsis;overflow:hidden}.VirtualScroll{position:relative;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch}.VirtualScroll__innerScrollContainer{box-sizing:border-box;overflow:hidden}.VirtualScroll__row{position:absolute}.proper-search{background-color:#fff}.proper-search .proper-search-field{font-size:14px;margin:0;line-height:22px;border:none}.proper-search .proper-search-field .proper-search-input{background:#fff;padding:3px 2px;background-color:#ccc;border-radius:3px 2px/2px 3px}.proper-search .proper-search-field .proper-search-input input[type=text]{width:100%;height:1.8em;margin-bottom:0;color:#777;padding-left:30px;padding-right:30px;border:none;outline:none;box-shadow:none;webkit-box-shadow:none}.proper-search .proper-search-field .proper-search-input i.proper-search-field-icon{color:#777;overflow:visible;margin-right:-20px;position:absolute;border:0;padding:4px}.proper-search .proper-search-field .proper-search-input .btn-clear{background:none;overflow:visible;position:absolute;border:0;padding-top:.2em;margin-left:-26px;outline:none}.proper-search .proper-search-field .proper-search-input .btn-clear i{color:#777}.proper-search .proper-search-field .proper-search-input .btn-clear:active,.proper-search .proper-search-field .proper-search-input .btn-clear:focus{box-shadow:none;webkit-box-shadow:none}.proper-search .proper-search-list .proper-search-list-bar{background-color:#f6f7f8;background-image:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjEuMCIgeDI9IjAuNSIgeTI9IjAuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==");background-size:100%;background-image:-webkit-gradient(linear,50% 100%,50% 0,color-stop(0,hsla(0,0%,90%,.61)),color-stop(100%,#fff));background-image:-webkit-linear-gradient(#fff,#efefef);background-image:linear-gradient(#fff,#efefef);padding:1px 0;width:100%;border:0;line-height:5px}.proper-search .proper-search-list .proper-search-list-bar .btn-group{width:100%}.proper-search .proper-search-list .proper-search-list-bar .btn-group label{position:relative;cursor:pointer;margin:0;font-weight:100;font-family:Helvetica;font-size:x-small}.proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select{display:inline-block;line-height:1.4;text-align:center;width:100%;cursor:pointer!important;border:none;margin:0;padding:5px 8px!important;border-radius:0;color:#333}.proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select.disabled,.proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select:active,.proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select:focus,.proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select:hover,.proper-search .proper-search-list .proper-search-list-bar .btn-group .btn-select[disabled]{background-color:#cecece;text-decoration:none;background-image:-webkit-gradient(linear,50% 100%,50% 0,color-stop(0,#cecece),color-stop(100%,#fff));background-image:-webkit-linear-gradient(#fff,#cecece);background-image:linear-gradient(#fff,#cecece)}.proper-search .proper-search-list .proper-search-list-virtual:active,.proper-search .proper-search-list .proper-search-list-virtual:focus{border:none;outline:none}.proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element{height:inherit;width:inherit;padding-left:.3em;margin-bottom:.1em;padding-top:.1em;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-sizing:border-box}.proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element.proper-search-selected i{color:#10ab10}.proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element.proper-search-single-selected{background-color:#c7ebff}.proper-search .proper-search-list .proper-search-list-virtual .proper-search-list-element i{width:14.5px;margin-right:8px}.proper-search .proper-search-loading{text-align:center;font-size:14px;margin:auto}
--------------------------------------------------------------------------------
/examples/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBIConsulting/ProperSearch/085bf4f1104ffa1290de5a6a41ddec63a4b77871/examples/favicon.ico
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ProperSearch - React Component made by Cbi developers
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/jsx/app.js:
--------------------------------------------------------------------------------
1 | import {Component} from 'react';
2 | import Search from "../../src/jsx/propersearch";
3 | import Normalizer from "../../src/jsx/utils/normalize";
4 | import {shallowEqualImmutable} from 'react-immutable-render-mixin';
5 |
6 | function getDefaultProps() {
7 | return {
8 | listHeight: 200,
9 | listRowHeight: 35
10 | }
11 | }
12 |
13 | class App extends Component {
14 |
15 | constructor(props) {
16 | super(props);
17 |
18 | this.state = {
19 | data: [],
20 | fieldsSet: null,
21 | selection: null,
22 | language: 'ENG',
23 | idField: 'value',
24 | displayField: 'label',
25 | defaultSearch: '',
26 | multiSelect: true,
27 | listHeight: this.props.listHeight,
28 | listRowHeight: this.props.listRowHeight,
29 | placeholder: 'Search placeHolder',
30 | filterOff: false,
31 | dataSize: 100,
32 | hidden: null
33 | }
34 | }
35 |
36 | componentWillMount() {
37 | let data = [], fieldsSet = null;
38 |
39 | for (let i = this.state.dataSize; i >= 0; i--) {
40 | if (i == 8 || i == 9 || i == 16) data.push({itemID: '', display: '', name: 'Tést ' + i, moreFields: 'moreFields values'});
41 | else data.push({itemID: 'item-' + i, display: this.formater.bind(this), name: 'Tést ' + i, moreFields: 'moreFields values'});
42 | };
43 |
44 | fieldsSet = new Set(_.keys(data[0]));
45 |
46 | this.setState({
47 | data: data,
48 | fieldsSet: fieldsSet,
49 | idField: 'itemID',
50 | displayField: 'display'
51 | });
52 | }
53 |
54 | shouldComponentUpdate(nextProps, nextState) {
55 | let stateChanged = !shallowEqualImmutable(this.state, nextState);
56 | let propsChanged = !shallowEqualImmutable(this.props, nextProps);
57 | let somethingChanged = propsChanged || stateChanged;
58 |
59 | if (nextState.dataSize != this.state.dataSize) {
60 | let newData = [];
61 |
62 | for (let i = nextState.dataSize; i >= 0; i--) {
63 | newData.push({
64 | [nextState.idField]: 'item-' + i,
65 | [nextState.displayField]: 'Item ' + i,
66 | name: 'Teeést ' + i,
67 | fieldx: 'xxx ' + i,
68 | fieldy: 'yyy ' + i
69 | });
70 | }
71 |
72 | this.setState({
73 | data: newData,
74 | fieldsSet: new Set(_.keys(newData[0]))
75 | });
76 |
77 | return false;
78 | }
79 |
80 | // If something change update form
81 | if (somethingChanged) {
82 | this.refs.listHeight.value = nextState.listHeight;
83 | this.refs.listElementHeight.value = nextState.listRowHeight;
84 | this.refs.idField.value = nextState.idField;
85 | this.refs.displayField.value = nextState.displayField;
86 | this.refs.dataSize.value = nextState.dataSize;
87 | this.refs.listElementHeight.value = nextState.listRowHeight;
88 | this.refs.lang.value = nextState.language;
89 | this.refs.multi.value = nextState.multiSelect;
90 | }
91 |
92 | return somethingChanged;
93 | }
94 |
95 | afterSearch(value) {
96 | console.info('Search: ', value);
97 | }
98 |
99 | afterSelect(data, selection) {
100 | console.info('Data: ', data);
101 | console.info('Selection: ', selection);
102 |
103 | this.setState({
104 | selection: selection
105 | });
106 | }
107 |
108 | filter(listElement, value) {
109 | let data = listElement.name;
110 | data = Normalizer.normalize(data);
111 | return data.indexOf(value) >= 0;
112 | }
113 |
114 | formater(listElement) {
115 | return {this.onButtonClick(e, listElement.name)} }>{ listElement.name } ;
116 | }
117 |
118 | onButtonClick(e, name) {
119 | console.log('Button ' + name + ' has been clicked');
120 | }
121 |
122 | onChangeData (e) {
123 | e.preventDefault();
124 | let data = [], fieldsSet = null, language = '', random = Math.floor(Math.random()* 10);
125 | let selection = ['item-'+ random,'item-' + (random+1)];
126 | let defaultSearch = 'Item '+ random, placeholder = 'Search Placeholder ' + random;
127 | let listHeight = this.props.listHeight + random, listRowHeight = this.props.listRowHeight + random;
128 | let multiSelect = !this.state.multiSelect, dataSize = (Math.floor(Math.random()* 1000) + 10);
129 |
130 | if (random % 2 == 0) language = 'ENG';
131 | else language = 'SPA';
132 |
133 | for (let i = dataSize; i >= 0; i--) {
134 | data.push({value: 'item-' + i, label: 'Item ' + i, name: 'Teeést ' + i, fieldx: 'xxx ' + i, fieldy: 'yyy ' + i});
135 | }
136 |
137 | fieldsSet = new Set(_.keys(data[0]));
138 |
139 | this.setState({
140 | data: data,
141 | fieldsSet: fieldsSet,
142 | idField: 'value',
143 | displayField: 'label',
144 | language: language,
145 | defaultSelection: selection,
146 | defaultSearch: defaultSearch,
147 | listHeight: listHeight,
148 | listRowHeight: listRowHeight,
149 | multiSelect: multiSelect,
150 | filter: null,
151 | placeholder: placeholder,
152 | afterSelect: this.afterSelect.bind(this),
153 | afterSearch: this.afterSearch.bind(this),
154 | dataSize: dataSize
155 | });
156 |
157 | }
158 |
159 | onChangeSize(e) {
160 | e.preventDefault();
161 | let size = this.refs.dataSize.value;
162 | size = parseInt(size);
163 |
164 | if (!isNaN(size)) {
165 | this.setState({
166 | dataSize: size
167 | });
168 | } else {
169 | this.refs.dataSize.value = this.state.dataSize;
170 | }
171 | }
172 |
173 | onChangeIdField(e) {
174 | e.preventDefault();
175 | let fieldsSet = this.state.fieldsSet, newIdField = this.refs.idField.value;
176 |
177 | // Data has this field so update state otherwise set field to current state value
178 | // (SEARCH Component has prevent this and throws an error message in console and don't update the idField if that field doesn't exist in data)
179 | if (fieldsSet.has(newIdField)) {
180 | this.setState({
181 | idField: newIdField
182 | });
183 | } else {
184 | console.error('The data has no field with the name ' + newIdField + '. The fields of the data are: ', fieldsSet);
185 | this.refs.idField.value = this.state.idField;
186 | }
187 | }
188 |
189 | onChangeDisplay(e) {
190 | e.preventDefault();
191 | let fieldsSet = this.state.fieldsSet, newDisplayField = this.refs.displayField.value;
192 |
193 | // Data has this field so update state otherwise set field to current state value
194 | // (SEARCH Component has prevent this and throws an error message in console and don't update the displayField if that field doesn't exist in data)
195 | if (fieldsSet.has(newDisplayField)) {
196 | this.setState({
197 | displayField: newDisplayField
198 | });
199 | } else {
200 | console.error('The data has no field with the name ' + newDisplayField + '. The fields of the data are: ', fieldsSet);
201 | this.refs.displayField.value = this.state.displayField;
202 | }
203 | }
204 |
205 | onChangeListHeight(e) {
206 | e.preventDefault();
207 | let height = this.refs.listHeight.value;
208 | height = parseInt(height);
209 |
210 | if (!isNaN(height)) {
211 | this.setState({
212 | listHeight: height
213 | });
214 | } else {
215 | this.refs.listHeight.value = this.state.listHeight;
216 | }
217 | }
218 |
219 | onChangeElementHeight(e) {
220 | e.preventDefault();
221 | let height = this.refs.listElementHeight.value;
222 | height = parseInt(height);
223 |
224 | if (!isNaN(height)) {
225 | this.setState({
226 | listRowHeight: height
227 | });
228 | } else {
229 | this.refs.listElementHeight.value = this.state.listRowHeight;
230 | }
231 | }
232 |
233 | onChangeMultiselect(e) {
234 | e.preventDefault();
235 | let multi = null, selection = this.state.selection ? this.state.selection[0] : null;
236 | if (this.refs.multi.value == 'true') multi = true;
237 | else multi = false;
238 |
239 | this.setState({
240 | multiSelect: multi,
241 | selection: selection
242 | });
243 | }
244 |
245 | onChangeLang(e) {
246 | e.preventDefault();
247 |
248 | this.setState({
249 | language: this.refs.lang.value
250 | });
251 | }
252 |
253 | apllyHidden(e) {
254 | e.preventDefault();
255 | let hidden = ["item-1", "item-2", "item-3"];
256 |
257 | if (this.state.hidden && this.state.hidden[0] === hidden[0]) {
258 | hidden = ["item-5", "item-6", "item-7"];
259 | }
260 |
261 | this.setState({
262 | hidden: hidden
263 | });
264 | }
265 |
266 | render() {
267 | let filter = !this.state.filterOff ? this.filter.bind(this) : null;
268 | let multiSelect = this.state.multiSelect, language = this.state.language;
269 |
270 | return (
271 |
272 |
273 |
274 |
Random Data
275 |
276 |
277 |
337 |
338 |
339 |
358 |
359 |
360 |
378 |
379 |
Get Empty values allowed (the empty values never get rendered "", just get that data after click the button, with selection [""])
380 |
381 |
382 |
383 |
384 | );
385 | }
386 | }
387 |
388 | App.defaultProps = getDefaultProps();
389 |
390 | export default App;
--------------------------------------------------------------------------------
/examples/jsx/example.js:
--------------------------------------------------------------------------------
1 | import App from "./app";
2 |
3 | const body = document.getElementById('body');
4 | ReactDOM.render( , body);
--------------------------------------------------------------------------------
/examples/screenshots/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBIConsulting/ProperSearch/085bf4f1104ffa1290de5a6a41ddec63a4b77871/examples/screenshots/example.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ProperSearch - React Component made by Cbi developers
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | module.exports = function(config) {
4 | config.set({
5 | browsers: ['PhantomJS'],
6 | files: [
7 | 'tests.webpack.js',
8 | {
9 | pattern: 'src/**/__tests__/*.js',
10 | included: false,
11 | served: false,
12 | watched: true
13 | }
14 | ],
15 | frameworks: ['jasmine'],
16 | preprocessors: {
17 | 'tests.webpack.js': ['webpack', 'sourcemap', 'coverage'],
18 | },
19 | reporters: ['progress', 'notification'],
20 | webpack: {
21 | devtool: 'inline-source-map',
22 | module: {
23 | loaders: [
24 | { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' }
25 | ]
26 | },
27 | plugins: [
28 | new webpack.DefinePlugin({
29 | 'process.env': {
30 | NODE_ENV: JSON.stringify('Test')
31 | }
32 | })
33 | ],
34 | watch: true
35 | },
36 | webpackServer: {
37 | noInfo: true,
38 | }
39 | });
40 | };
--------------------------------------------------------------------------------
/lib/components/__tests__/search-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _search = require('../search');
4 |
5 | var _search2 = _interopRequireDefault(_search);
6 |
7 | var _reactAddonsTestUtils = require('react-addons-test-utils');
8 |
9 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils);
10 |
11 | var _react = require('react');
12 |
13 | var _react2 = _interopRequireDefault(_react);
14 |
15 | var _reactDom = require('react-dom');
16 |
17 | var _reactDom2 = _interopRequireDefault(_reactDom);
18 |
19 | var _jquery = require('jquery');
20 |
21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
22 |
23 | var Set = require('es6-set');
24 |
25 | describe('Search', function () {
26 | var wrapper = null;
27 |
28 | beforeEach(function () {
29 | wrapper = document.createElement('div');
30 | });
31 |
32 | it('is available', function () {
33 | expect(_search2['default']).not.toBe(null);
34 | });
35 |
36 | it('filter', function (done) {
37 | var def = (0, _jquery.Deferred)(),
38 | component = null,
39 | node = null,
40 | props = getProps();
41 | props.afterSearch = function (searchValue) {
42 | if (searchValue) def.resolve(searchValue);
43 | };
44 |
45 | component = prepare(props);
46 |
47 | // Check filter
48 | component.handleSearch('foo');
49 |
50 | def.done(function (value) {
51 | expect(component.state.data.toJSON()[0].itemID).toBe('3'); // Testing filter
52 | expect(value).toBe('foo');
53 | }).always(done);
54 | });
55 |
56 | it('selection multiselect', function (done) {
57 | var def = (0, _jquery.Deferred)(),
58 | component = null,
59 | props = getProps();
60 | props.afterSelect = function (data, selection) {
61 | def.resolve(data, selection);
62 | };
63 | component = prepare(props);
64 |
65 | // Check selection
66 | component.triggerSelection(new Set(['1', '2']));
67 |
68 | def.done(function (data, selection) {
69 | expect(data.length).toBe(2);
70 | expect(selection.length).toBe(2);
71 | expect(data).toContain({ itemID: 1, display: 'Test1', toFilter: 'fee' });
72 | expect(data[2]).not.toBeDefined();
73 | expect(selection).toContain('1');
74 | expect(selection).toContain('2');
75 | }).always(done);
76 | });
77 |
78 | it('selection multiselect display-function', function (done) {
79 | var def = (0, _jquery.Deferred)(),
80 | component = null,
81 | props = getPropsBigData();
82 |
83 | props.multiSelect = true;
84 | props.afterSelect = function (data, selection) {
85 | def.resolve(data, selection);
86 | };
87 | component = prepare(props);
88 |
89 | // Check selection
90 | component.triggerSelection(new Set(['1', 'item_2', 'item_5', 'item_6', 'item_8']));
91 |
92 | def.done(function (data, selection) {
93 | expect(data.length).toBe(4);
94 | expect(selection.length).toBe(5);
95 | expect(selection).toContain('1');
96 | expect(selection).toContain('item_2');
97 | expect(selection).toContain('item_6');
98 | expect(selection).not.toContain('item_4');
99 | expect(data).toContain({ itemID: 'item_8', display: formater, name: 'Tést 8', moreFields: 'moreFields values' });
100 | }).always(done);
101 | });
102 |
103 | it('selection singleselection', function (done) {
104 | var def = (0, _jquery.Deferred)(),
105 | def2 = (0, _jquery.Deferred)(),
106 | component = null,
107 | props = getPropsBigData();
108 |
109 | props.defaultSelection = null;
110 | props.afterSelect = function (data, selection) {
111 | if (data.length >= 1) {
112 | def.resolve(data, selection);
113 | } else {
114 | def2.resolve(data, selection);
115 | }
116 | };
117 | component = prepare(props);
118 | component.triggerSelection(new Set(['item_190']));
119 |
120 | def.done(function (data, selection) {
121 | expect(data.length).toBe(1);
122 | expect(selection.length).toBe(1);
123 | expect(selection[0]).toBe('item_190');
124 | });
125 |
126 | component.triggerSelection(new Set([]));
127 | def2.done(function (data, selection) {
128 | expect(data.length).toBe(0);
129 | expect(selection.length).toBe(0);
130 | }).always(done);
131 | });
132 |
133 | it('selectAll on filtered data', function (done) {
134 | var def = (0, _jquery.Deferred)(),
135 | component = null,
136 | node = null,
137 | props = null;
138 | props = getPropsBigData();
139 |
140 | props.multiSelect = true;
141 | props.defaultSelection = null;
142 | props.afterSelect = function (data, selection) {
143 | if (data.length > 1) {
144 | def.resolve(data, selection);
145 | }
146 | };
147 |
148 | component = prepare(props);
149 | node = _reactAddonsTestUtils2['default'].findRenderedDOMComponentWithClass(component, "list-bar-check");
150 |
151 | component.handleSearch('test 10'); // Filter
152 | _reactAddonsTestUtils2['default'].Simulate.click(node); // Select All
153 | component.handleSearch(null); // Back to default
154 |
155 | def.done(function (data, selection) {
156 | expect(selection.length).toBe(12);
157 | expect(data.length).toBe(12);
158 | }).always(done);
159 | });
160 |
161 | it('select/unselect all on filtered data && multiple operations', function (done) {
162 | var def = (0, _jquery.Deferred)(),
163 | component = null,
164 | nodeCheckAll = null,
165 | nodeUnCheck = null,
166 | nodeElements = null,
167 | props = null,
168 | promise = null;
169 | props = getPropsBigData();
170 | promise = { done: function done() {
171 | return;
172 | } };
173 |
174 | spyOn(promise, 'done');
175 |
176 | props.multiSelect = true;
177 | props.defaultSelection = null;
178 | props.afterSelect = function (data, selection) {
179 | if (promise.done.calls.any()) {
180 | def.resolve(data, selection);
181 | }
182 | };
183 |
184 | component = prepare(props);
185 | nodeCheckAll = _reactAddonsTestUtils2['default'].findRenderedDOMComponentWithClass(component, "list-bar-check");
186 | nodeUnCheck = _reactAddonsTestUtils2['default'].findRenderedDOMComponentWithClass(component, "list-bar-unCheck");
187 | nodeElements = _reactAddonsTestUtils2['default'].scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
188 |
189 | _reactAddonsTestUtils2['default'].Simulate.click(nodeCheckAll); // Select All 1000
190 | component.handleSearch('test 10'); // Filter (12 elements 1000 110 109... 100 10)
191 | _reactAddonsTestUtils2['default'].Simulate.click(nodeUnCheck); // UnSelect All (1000 - 12 => 988)
192 | component.handleSearch(null); // Back to default
193 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[3]); // Unselect element 997 (988 - 1 => 987)
194 | component.handleSearch('test 11'); // Filter (11 elements 11 110 111... 116 117 118 119)
195 | _reactAddonsTestUtils2['default'].Simulate.click(nodeUnCheck); // UnSelect All (987 - 11 => 976)
196 | _reactAddonsTestUtils2['default'].Simulate.click(nodeCheckAll); // Select All (976 + 11 => 987)
197 | promise.done();
198 | nodeElements = _reactAddonsTestUtils2['default'].scryRenderedDOMComponentsWithClass(component, "proper-search-list-element"); // update nodeElement to current in view
199 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[3]); // Click element 116 (unSelect) (987 - 1 => 986)
200 |
201 | def.done(function (data, selection) {
202 | var set = new Set(selection);
203 |
204 | expect(selection.length).toBe(986);
205 | expect(data.length).toBe(986);
206 | expect(set.has('item_997')).toBe(false);
207 | expect(set.has('item_996')).toBe(true);
208 | expect(set.has('item_116')).toBe(false);
209 | expect(set.has('item_100')).toBe(false);
210 | expect(promise.done).toHaveBeenCalled();
211 | }).always(done);
212 | });
213 |
214 | it('keeps selection after refreshing data && update props', function (done) {
215 | var def = (0, _jquery.Deferred)(),
216 | props = getPropsBigData(),
217 | promise = null,
218 | component = null,
219 | nodeCheckAll = null,
220 | nodeElements = null,
221 | newData = [];
222 | promise = { done: function done() {
223 | return;
224 | } };
225 |
226 | spyOn(promise, 'done');
227 |
228 | props.multiSelect = true;
229 | props.defaultSelection = null;
230 | props.afterSelect = function (data, selection) {
231 | if (promise.done.calls.any()) {
232 | def.resolve(data, selection);
233 | }
234 | };
235 |
236 | component = _reactDom2['default'].render(_react2['default'].createElement(_search2['default'], props), wrapper);
237 |
238 | nodeCheckAll = _reactAddonsTestUtils2['default'].findRenderedDOMComponentWithClass(component, "list-bar-check");
239 | _reactAddonsTestUtils2['default'].Simulate.click(nodeCheckAll); // Select All 1000
240 |
241 | for (var i = 1000; i > 0; i--) {
242 | newData.push({ id: 'item_' + i, display2: 'Element_' + i, name2: 'Testing2 ' + i });
243 | };
244 | props.data = newData;
245 | props.idField = 'id';
246 | props.displayField = 'display2';
247 | props.filterField = 'name2';
248 |
249 | component = _reactDom2['default'].render(_react2['default'].createElement(_search2['default'], props), wrapper); // Update props
250 | nodeElements = _reactAddonsTestUtils2['default'].scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
251 |
252 | promise.done();
253 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[0]); // Unselect one element (Element 1000) to call afterSelect
254 |
255 | def.done(function (data, selection) {
256 | expect(selection.length).toBe(999);
257 | expect(data[0]).toEqual(newData[1]);
258 | expect(data[0].id).toBeDefined();
259 | expect(data[0].display2).toBeDefined();
260 | expect(data[0].name2).toBeDefined();
261 | expect(data[0].moreFields).not.toBeDefined();
262 | }).always(done);
263 | });
264 | });
265 |
266 | function prepare(props) {
267 | return _reactAddonsTestUtils2['default'].renderIntoDocument(_react2['default'].createElement(_search2['default'], props));
268 | }
269 |
270 | function getProps() {
271 | return {
272 | data: [{ itemID: 1, display: 'Test1', toFilter: 'fee' }, { itemID: 2, display: 'Test2', toFilter: 'fuu' }, { itemID: 3, display: 'Test3', toFilter: 'foo' }],
273 | lang: 'SPA',
274 | defaultSelection: [1],
275 | multiSelect: true,
276 | idField: 'itemID',
277 | displayField: 'display',
278 | filterField: 'toFilter',
279 | listWidth: 100,
280 | listHeight: 100
281 | };
282 | }
283 |
284 | function formater(listElement) {
285 | return _react2['default'].createElement(
286 | 'button',
287 | { className: 'btn btn-default' },
288 | listElement.name
289 | );
290 | }
291 |
292 | function getPropsBigData() {
293 | var length = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1000;
294 |
295 | var data = [];
296 |
297 | for (var i = length; i > 0; i--) {
298 | data.push({ itemID: 'item_' + i, display: formater, name: 'Tést ' + i, moreFields: 'moreFields values' });
299 | };
300 |
301 | return {
302 | data: data,
303 | idField: 'itemID',
304 | defaultSelection: ['item_1'],
305 | displayField: 'display',
306 | filterField: 'name',
307 | listWidth: 100,
308 | listHeight: 100
309 | };
310 | }
--------------------------------------------------------------------------------
/lib/components/__tests__/searchList-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _searchList = require('../searchList');
4 |
5 | var _searchList2 = _interopRequireDefault(_searchList);
6 |
7 | var _immutable = require('immutable');
8 |
9 | var _immutable2 = _interopRequireDefault(_immutable);
10 |
11 | var _reactAddonsTestUtils = require('react-addons-test-utils');
12 |
13 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils);
14 |
15 | var _react = require('react');
16 |
17 | var _react2 = _interopRequireDefault(_react);
18 |
19 | var _reactDom = require('react-dom');
20 |
21 | var _reactDom2 = _interopRequireDefault(_reactDom);
22 |
23 | var _underscore = require('underscore');
24 |
25 | var _underscore2 = _interopRequireDefault(_underscore);
26 |
27 | var _jquery = require('jquery');
28 |
29 | var _messages = require('../../lang/messages');
30 |
31 | var _messages2 = _interopRequireDefault(_messages);
32 |
33 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
34 |
35 | var Set = require('es6-set');
36 |
37 | describe('SearchList', function () {
38 |
39 | it('is available', function () {
40 | expect(_searchList2['default']).not.toBe(null);
41 | });
42 |
43 | it('handleElementClick singleSelection', function (done) {
44 | var def = (0, _jquery.Deferred)();
45 | var props = getPropsBigData();
46 | var expected = 'item_89';
47 | props.multiSelect = false;
48 | props.onSelectionChange = function (selection) {
49 | def.resolve(selection);
50 | };
51 |
52 | var component = prepare(props);
53 |
54 | // Click elements
55 | var nodeElements = _reactAddonsTestUtils2['default'].scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
56 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[11]);
57 |
58 | def.done(function (selection) {
59 | expect(selection.has(expected)).toBe(true);
60 | expect(selection.size).toBe(1);
61 | }).always(done);
62 | });
63 |
64 | it('handleElementClick multiselect', function (done) {
65 | var def = (0, _jquery.Deferred)();
66 | var props = getPropsBigData();
67 | var expected = new Set(['item_98', 'item_100', 'item_88', 'item_91']);
68 |
69 | props.onSelectionChange = function (selection) {
70 | if (selection.size >= 1) {
71 | def.resolve(selection);
72 | }
73 | };
74 |
75 | var component = prepare(props);
76 |
77 | // Click elements
78 | var nodeElements = _reactAddonsTestUtils2['default'].scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
79 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[2]);
80 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[1]); // Old selected item (unselect now)
81 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[0]);
82 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[12]);
83 | _reactAddonsTestUtils2['default'].Simulate.click(nodeElements[9]);
84 |
85 | def.done(function (selection) {
86 | var result = true;
87 | selection.forEach(function (element) {
88 | if (!expected.has(element)) {
89 | result = false;
90 | return;
91 | }
92 | });
93 | expect(result).toBe(true);
94 | expect(selection.size).toBe(4);
95 | }).always(done);
96 | });
97 |
98 | it('handleSelectAll all', function (done) {
99 | var def = (0, _jquery.Deferred)();
100 | var props = getPropsBigData();
101 |
102 | props.onSelectionChange = function (selection) {
103 | def.resolve(selection);
104 | };
105 |
106 | var component = prepare(props);
107 |
108 | // Click elements
109 | var node = _reactAddonsTestUtils2['default'].findRenderedDOMComponentWithClass(component, "list-bar-check");
110 | _reactAddonsTestUtils2['default'].Simulate.click(node);
111 |
112 | def.done(function (selection) {
113 | expect(selection.size).toBe(100);
114 | }).always(done);
115 | });
116 |
117 | it('handleSelectAll nothing', function (done) {
118 | var def = (0, _jquery.Deferred)();
119 | var props = getPropsBigData();
120 |
121 | props.onSelectionChange = function (selection) {
122 | def.resolve(selection);
123 | };
124 |
125 | var component = prepare(props);
126 |
127 | // Click elements
128 | var node = _reactAddonsTestUtils2['default'].findRenderedDOMComponentWithClass(component, "list-bar-unCheck");
129 | _reactAddonsTestUtils2['default'].Simulate.click(node);
130 |
131 | def.done(function (selection) {
132 | expect(selection.size).toBe(0);
133 | }).always(done);
134 | });
135 | });
136 |
137 | function prepare(props) {
138 | return _reactAddonsTestUtils2['default'].renderIntoDocument(_react2['default'].createElement(_searchList2['default'], props));
139 | }
140 |
141 | function formater(listElement) {
142 | return _react2['default'].createElement(
143 | 'button',
144 | { className: 'btn btn-default' },
145 | listElement.name
146 | );
147 | }
148 |
149 | function getPropsBigData() {
150 | var length = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 100;
151 |
152 | var data = [],
153 | preparedData = null;
154 |
155 | for (var i = length; i > 0; i--) {
156 | data.push({ itemID: 'item_' + i, display: formater, name: 'Tést ' + i, moreFields: 'moreFields values' });
157 | };
158 |
159 | preparedData = prepareData(data, 'itemID');
160 |
161 | return {
162 | data: preparedData.data,
163 | indexedData: preparedData.indexed,
164 | idField: 'itemID',
165 | multiSelect: true,
166 | selection: new Set(['item_99']),
167 | displayField: 'display',
168 | uniqueID: 'test',
169 | messages: _messages2['default']['ENG']
170 | };
171 | }
172 |
173 | /*
174 | * Prepare the data received by the component for the internal working.
175 | */
176 | function prepareData(newData, field) {
177 | // The data will be inmutable inside the component
178 | var data = _immutable2['default'].fromJS(newData),
179 | index = 0;
180 | var indexed = [],
181 | parsed = [];
182 |
183 | // Parsing data to add new fields (selected or not, field, rowIndex)
184 | parsed = data.map(function (row) {
185 | if (!row.get(field, false)) {
186 | row = row.set(field, _underscore2['default'].uniqueId());
187 | } else {
188 | row = row.set(field, row.get(field).toString());
189 | }
190 |
191 | if (!row.get('_selected', false)) {
192 | row = row.set('_selected', false);
193 | }
194 |
195 | row = row.set('_rowIndex', index++);
196 |
197 | return row;
198 | });
199 |
200 | // Prepare indexed data.
201 | indexed = _underscore2['default'].indexBy(parsed.toJSON(), field);
202 |
203 | return {
204 | rawdata: data,
205 | data: parsed,
206 | indexed: indexed
207 | };
208 | }
--------------------------------------------------------------------------------
/lib/components/search.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
8 |
9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
10 |
11 | var _react = require('react');
12 |
13 | var _react2 = _interopRequireDefault(_react);
14 |
15 | var _immutable = require('immutable');
16 |
17 | var _immutable2 = _interopRequireDefault(_immutable);
18 |
19 | var _underscore = require('underscore');
20 |
21 | var _underscore2 = _interopRequireDefault(_underscore);
22 |
23 | var _searchList = require('./searchList');
24 |
25 | var _searchList2 = _interopRequireDefault(_searchList);
26 |
27 | var _reactPropersearchField = require('react-propersearch-field');
28 |
29 | var _reactPropersearchField2 = _interopRequireDefault(_reactPropersearchField);
30 |
31 | var _messages2 = require('../lang/messages');
32 |
33 | var _messages3 = _interopRequireDefault(_messages2);
34 |
35 | var _normalize = require('../utils/normalize');
36 |
37 | var _normalize2 = _interopRequireDefault(_normalize);
38 |
39 | var _reactImmutableRenderMixin = require('react-immutable-render-mixin');
40 |
41 | var _cache = require('../lib/cache');
42 |
43 | var _cache2 = _interopRequireDefault(_cache);
44 |
45 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
46 |
47 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
48 |
49 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
50 |
51 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
52 |
53 | var Set = require('es6-set');
54 |
55 | // For more info about this read ReadMe.md
56 | function getDefaultProps() {
57 | return {
58 | data: [],
59 | rawdata: null, // Case you want to use your own inmutable data. Read prepareData() method for more info.
60 | indexed: null, // Case you want to use your own inmutable data. Read prepareData() method for more info.
61 | messages: _messages3['default'],
62 | lang: 'ENG',
63 | rowFormater: null, // function to format values in render
64 | defaultSelection: null,
65 | hiddenSelection: null,
66 | multiSelect: false,
67 | listWidth: null,
68 | listHeight: 200,
69 | listRowHeight: 26,
70 | afterSelect: null, // Function Get selection and data
71 | afterSelectGetSelection: null, // Function Get just selection (no data)
72 | afterSearch: null,
73 | onEnter: null, // Optional - To do when key down Enter - SearchField
74 | fieldClass: null,
75 | listClass: null,
76 | listElementClass: null,
77 | className: null,
78 | placeholder: 'Search...',
79 | searchIcon: 'fa fa-search fa-fw',
80 | clearIcon: 'fa fa-times fa-fw',
81 | throttle: 160, // milliseconds
82 | minLength: 3,
83 | defaultSearch: null,
84 | autoComplete: 'off',
85 | idField: 'value',
86 | displayField: 'label',
87 | listShowIcon: true,
88 | filter: null, // Optional function (to be used when the displayField is an function too)
89 | filterField: null, // By default it will be the displayField
90 | allowsEmptySelection: false };
91 | }
92 |
93 | /**
94 | * A proper search component for react. With a search field and a list of items allows the user to filter that list and select the items.
95 | * The component return the selected data when it's selected. Allows multi and single selection. The list is virtual rendered, was designed
96 | * to handle thousands of elements without sacrificing performance, just render the elements in the view. Used react-virtualized to render the list items.
97 | *
98 | * Simple example usage:
99 | *
100 | * let data = [];
101 | * data.push({
102 | * value: 1,
103 | * label: 'Apple'
104 | * });
105 | *
106 | * let afterSelect = (data, selection) => {
107 | * console.info(data);
108 | * console.info(selection);
109 | * }
110 | *
111 | *
116 | * ```
117 | */
118 | // Put this to true to get a diferent ToolBar that allows select empty
119 |
120 | var Search = function (_React$Component) {
121 | _inherits(Search, _React$Component);
122 |
123 | function Search(props) {
124 | _classCallCheck(this, Search);
125 |
126 | var _this = _possibleConstructorReturn(this, (Search.__proto__ || Object.getPrototypeOf(Search)).call(this, props));
127 |
128 | var preparedData = _this.prepareData(null, props.idField, false, props.displayField);
129 |
130 | _this.state = {
131 | data: preparedData.data, // Data to work with (Inmutable)
132 | initialData: preparedData.data, // Same data as initial state.data but this data never changes. (Inmutable)
133 | rawData: preparedData.rawdata, // Received data without any modfication (Inmutable)
134 | indexedData: preparedData.indexed, // Received data indexed (No Inmutable)
135 | initialIndexed: preparedData.indexed, // When data get filtered keep the full indexed
136 | idField: props.idField, // To don't update the idField if that field doesn't exist in the fields of data array
137 | displayField: props.displayField, // same
138 | selection: new Set(),
139 | allSelected: false,
140 | selectionApplied: false, // If the selection has been aplied to the data (mostly for some cases of updating props data)
141 | ready: false
142 | };
143 | return _this;
144 | }
145 |
146 | _createClass(Search, [{
147 | key: 'componentDidMount',
148 | value: function componentDidMount() {
149 | this.setDefaultSelection(this.props.defaultSelection);
150 |
151 | this.setState({
152 | ready: true
153 | });
154 | }
155 | }, {
156 | key: 'shouldComponentUpdate',
157 | value: function shouldComponentUpdate(nextProps, nextState) {
158 | var _this2 = this;
159 |
160 | var stateChanged = !(0, _reactImmutableRenderMixin.shallowEqualImmutable)(this.state, nextState);
161 | var propsChanged = !(0, _reactImmutableRenderMixin.shallowEqualImmutable)(this.props, nextProps);
162 | var somethingChanged = propsChanged || stateChanged;
163 |
164 | // Update row indexes when data get filtered
165 | if (this.state.data.size != nextState.data.size) {
166 | var parsed = undefined,
167 | indexed = undefined,
168 | data = undefined;
169 |
170 | if (nextState.ready) {
171 | if (nextState.data.size === 0) {
172 | data = nextState.data;
173 | indexed = {};
174 | } else {
175 | parsed = this.prepareData(nextState.data, this.state.idField, true, this.state.displayField); // Force rebuild indexes etc
176 | data = parsed.data;
177 | indexed = parsed.indexed;
178 | }
179 |
180 | this.setState({
181 | data: data,
182 | indexedData: indexed,
183 | allSelected: this.isAllSelected(data, nextState.selection)
184 | });
185 | } else {
186 | var selection = nextProps.defaultSelection;
187 | if (!nextProps.multiSelect) selection = nextState.selection.values().next().value || null;
188 |
189 | // props data has been changed in the last call to this method
190 | this.setDefaultSelection(selection);
191 | if (_underscore2['default'].isNull(selection) || selection.length === 0) this.setState({ ready: true }); // No def selection so then ready
192 | }
193 |
194 | return false;
195 | }
196 |
197 | if (propsChanged) {
198 | var _ret = function () {
199 | var dataChanged = !(0, _reactImmutableRenderMixin.shallowEqualImmutable)(_this2.props.data, nextProps.data);
200 | var idFieldChanged = _this2.props.idField != nextProps.idField,
201 | displayFieldChanged = _this2.props.displayField != nextProps.displayField;
202 | var selectionChanged = false,
203 | nextSelection = new Set(nextProps.defaultSelection),
204 | selection = null;
205 |
206 | if (_this2.state.selection.size != nextSelection.size) {
207 | selectionChanged = true;
208 | selection = nextProps.defaultSelection;
209 | } else {
210 | _this2.state.selection.forEach(function (element) {
211 | if (!nextSelection.has(element)) {
212 | selectionChanged = true;
213 | selection = nextProps.defaultSelection;
214 | return true;
215 | }
216 | });
217 | }
218 |
219 | if (!nextProps.multiSelect && (nextSelection.size > 1 || _this2.state.selection.size > 1)) {
220 | selection = nextSelection.size > 1 ? nextProps.defaultSelection : _this2.state.selection.values().next().value;
221 | if (_underscore2['default'].isArray(selection)) selection = selection[0];
222 | }
223 |
224 | if (idFieldChanged || displayFieldChanged) {
225 | var fieldsSet = new Set(_underscore2['default'].keys(nextProps.data[0]));
226 | var _messages = _this2.getTranslatedMessages();
227 |
228 | // Change idField / displayField but that field doesn't exist in the data
229 | if (!fieldsSet.has(nextProps.idField) || !fieldsSet.has(nextProps.displayField)) {
230 | if (!fieldsSet.has(nextProps.idField)) console.error(_messages.errorIdField + ' ' + nextProps.idField + ' ' + _messages.errorData);else console.error(_messages.errorDisplayField + ' ' + nextProps.idField + ' ' + _messages.errorData);
231 |
232 | return {
233 | v: false
234 | };
235 | } else {
236 | // New idField &&//|| displayField exist in data array fields
237 | if (dataChanged) {
238 | _cache2['default'].flush('search_list');
239 | var preparedData = _this2.prepareData(nextProps.data, nextProps.idField, false, nextProps.displayField);
240 |
241 | _this2.setState({
242 | data: preparedData.data,
243 | initialData: preparedData.data,
244 | rawData: preparedData.rawdata,
245 | indexedData: preparedData.indexed,
246 | initialIndexed: preparedData.indexed,
247 | idField: nextProps.idField,
248 | displayField: nextProps.displayField,
249 | ready: false,
250 | selectionApplied: false
251 | }, _this2.setDefaultSelection(selection));
252 | } else {
253 | var initialIndexed = null,
254 | _indexed = null;
255 |
256 | // If the id field change then the indexed data has to be changed but not for display
257 | if (displayFieldChanged) {
258 | initialIndexed = _this2.state.initialIndexed;
259 | _indexed = _this2.state.indexedData;
260 | } else {
261 | initialIndexed = _underscore2['default'].indexBy(_this2.state.initialData.toJSON(), nextProps.idField);
262 | _indexed = _underscore2['default'].indexBy(_this2.state.data.toJSON(), nextProps.idField);
263 | }
264 |
265 | _this2.setState({
266 | indexedData: _indexed,
267 | initialIndexed: initialIndexed,
268 | idField: nextProps.idField,
269 | displayField: nextProps.displayField,
270 | ready: false,
271 | selectionApplied: false
272 | });
273 | }
274 | return {
275 | v: false
276 | };
277 | }
278 | }
279 |
280 | if (dataChanged) {
281 | _cache2['default'].flush('search_list');
282 | var _preparedData = _this2.prepareData(nextProps.data, nextProps.idField, false, nextProps.displayField);
283 |
284 | _this2.setState({
285 | data: _preparedData.data,
286 | initialData: _preparedData.data,
287 | rawData: _preparedData.rawdata,
288 | indexedData: _preparedData.indexed,
289 | initialIndexed: _preparedData.indexed,
290 | ready: false,
291 | selectionApplied: false
292 | }, _this2.setDefaultSelection(selection));
293 |
294 | return {
295 | v: false
296 | };
297 | }
298 |
299 | if (selectionChanged) {
300 | // Default selection does nothing if the selection is null so in that case update the state to restart selection
301 | if (!_underscore2['default'].isNull(selection)) {
302 | _this2.setDefaultSelection(selection);
303 | } else {
304 | _this2.setState({
305 | selection: new Set(),
306 | allSelected: false,
307 | ready: true
308 | });
309 | }
310 |
311 | return {
312 | v: false
313 | };
314 | }
315 | }();
316 |
317 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v;
318 | }
319 |
320 | if (!nextState.ready) {
321 | this.setState({
322 | ready: true
323 | });
324 |
325 | return false;
326 | }
327 |
328 | return somethingChanged;
329 | }
330 |
331 | /**
332 | * Before the components update set the updated selection data to the components state.
333 | *
334 | * @param {object} nextProps The props that will be set for the updated component
335 | * @param {object} nextState The state that will be set for the updated component
336 | */
337 |
338 | }, {
339 | key: 'componentWillUpdate',
340 | value: function componentWillUpdate(nextProps, nextState) {
341 | // Selection
342 | if (this.props.multiSelect) {
343 | if (nextState.selection.size !== this.state.selection.size || !nextState.selectionApplied && nextState.selection.size > 0) {
344 | this.updateSelectionData(nextState.selection, nextState.allSelected);
345 | }
346 | } else {
347 | var next = nextState.selection.values().next().value || null;
348 | var old = this.state.selection.values().next().value || null;
349 | var oldSize = !_underscore2['default'].isNull(this.state.selection) ? this.state.selection.size : 0;
350 |
351 | if (next !== old || oldSize > 1) {
352 | this.updateSelectionData(next);
353 | }
354 | }
355 | }
356 |
357 | /**
358 | * Method called before the components update to set the new selection to states component and update the data
359 | *
360 | * @param {array} newSelection The new selected rows (Set object)
361 | * @param {array} newAllSelected If the new state has all the rows selected
362 | */
363 |
364 | }, {
365 | key: 'updateSelectionData',
366 | value: function updateSelectionData(newSelection) {
367 | var _this3 = this;
368 |
369 | var newAllSelected = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
370 |
371 | var newIndexed = _underscore2['default'].clone(this.state.indexedData);
372 | var oldSelection = this.state.selection;
373 | var rowid = null,
374 | selected = null,
375 | rdata = null,
376 | curIndex = null,
377 | newData = this.state.data,
378 | rowIndex = null;
379 | var newSelectionSize = !_underscore2['default'].isNull(newSelection) ? newSelection.size : 0;
380 |
381 | // If oldSelection size is bigger than 1 that mean's the props has changed from multiselect to single select so if there is some list items with the selected class
382 | // if should be reset.
383 | if (!this.props.multiSelect && oldSelection.size <= 1) {
384 | // Single select
385 | var oldId = oldSelection.values().next().value || null;
386 | var indexedKeys = new Set(_underscore2['default'].keys(newIndexed));
387 |
388 | if (!_underscore2['default'].isNull(oldId) && indexedKeys.has(oldId)) {
389 | newIndexed[oldId]._selected = false; // Update indexed data
390 | rowIndex = newIndexed[oldId]._rowIndex; // Get data index
391 | if (newData.get(rowIndex)) {
392 | rdata = newData.get(rowIndex).set('_selected', false); // Change the row in that index
393 | newData = newData.set(rowIndex, rdata); // Set that row in the data object
394 | }
395 | }
396 |
397 | if (!_underscore2['default'].isNull(newSelection) && indexedKeys.has(newSelection)) {
398 | newIndexed[newSelection]._selected = true; // Update indexed data
399 | rowIndex = newIndexed[newSelection]._rowIndex; // Get data index
400 | rdata = newData.get(rowIndex).set('_selected', true); // Change the row in that index
401 | newData = newData.set(rowIndex, rdata); // Set that row in the data object
402 | }
403 | } else if (!newAllSelected && this.isSingleChange(newSelectionSize)) {
404 | // Change one row data at a time
405 | var changedId = null,
406 | _selected = null;
407 |
408 | // If the new selection has not one of the ids of the old selection that means an selected element has been unselected.
409 | oldSelection.forEach(function (id) {
410 | if (!newSelection.has(id)) {
411 | changedId = id;
412 | _selected = false;
413 | return false;
414 | }
415 | });
416 |
417 | // Otherwise a new row has been selected. Look through the new selection for the new element.
418 | if (!changedId) {
419 | _selected = true;
420 | newSelection.forEach(function (id) {
421 | if (!oldSelection.has(id)) {
422 | changedId = id;
423 | return false;
424 | }
425 | });
426 | }
427 |
428 | newIndexed[changedId]._selected = _selected; // Update indexed data
429 | rowIndex = newIndexed[changedId]._rowIndex; // Get data index
430 | rdata = newData.get(rowIndex).set('_selected', _selected); // Change the row in that index
431 | newData = newData.set(rowIndex, rdata); // Set that row in the data object
432 | } else {
433 | // Change all data
434 | if (_underscore2['default'].isNull(newSelection)) newSelection = new Set();else if (!_underscore2['default'].isObject(newSelection)) newSelection = new Set([newSelection]);
435 |
436 | newData = newData.map(function (row) {
437 | rowid = row.get(_this3.state.idField);
438 | selected = newSelection.has(rowid.toString());
439 | rdata = row.set('_selected', selected);
440 | curIndex = newIndexed[rowid];
441 |
442 | if (curIndex._selected != selected) {
443 | // update indexed data
444 | curIndex._selected = selected;
445 | newIndexed[rowid] = curIndex;
446 | }
447 |
448 | return rdata;
449 | });
450 | }
451 |
452 | this.setState({
453 | data: newData,
454 | indexedData: newIndexed,
455 | selectionApplied: true
456 | });
457 | }
458 |
459 | /**
460 | * Check if the selection has more than 1 change.
461 | *
462 | * @param {integer} newSize Size of the new selection
463 | */
464 |
465 | }, {
466 | key: 'isSingleChange',
467 | value: function isSingleChange(newSize) {
468 | var oldSize = this.state.selection.size;
469 |
470 | if (oldSize - 1 == newSize || oldSize + 1 == newSize) return true;else return false;
471 | }
472 |
473 | /**
474 | * Get the translated messages for the component.
475 | *
476 | * @return object Messages of the selected language or in English if the translation for this lang doesn't exist.
477 | */
478 |
479 | }, {
480 | key: 'getTranslatedMessages',
481 | value: function getTranslatedMessages() {
482 | if (!_underscore2['default'].isObject(this.props.messages)) {
483 | return {};
484 | }
485 |
486 | if (this.props.messages[this.props.lang]) {
487 | return this.props.messages[this.props.lang];
488 | }
489 |
490 | return this.props.messages['ENG'];
491 | }
492 |
493 | /**
494 | * In case that the new selection array be different than the selection array in the components state, then update
495 | * the components state with the new data.
496 | *
497 | * @param {array} newSelection The selected rows
498 | * @param {boolean} sendSelection If the selection must be sent or not
499 | */
500 |
501 | }, {
502 | key: 'triggerSelection',
503 | value: function triggerSelection() {
504 | var newSelection = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Set();
505 | var sendSelection = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
506 |
507 | if (sendSelection) {
508 | this.setState({
509 | selection: newSelection,
510 | allSelected: this.isAllSelected(this.state.data, newSelection)
511 | }, this.sendSelection);
512 | } else {
513 | this.setState({
514 | selection: newSelection,
515 | allSelected: this.isAllSelected(this.state.data, newSelection)
516 | });
517 | }
518 | }
519 |
520 | /**
521 | * Check if all the current data are selected.
522 | *
523 | * @param {array} data The data to compare with selection
524 | * @param {object} selection The current selection Set of values (idField)
525 | */
526 |
527 | }, {
528 | key: 'isAllSelected',
529 | value: function isAllSelected(data, selection) {
530 | var _this4 = this;
531 |
532 | var result = true;
533 | if (data.size > selection.size) return false;
534 |
535 | data.forEach(function (item, index) {
536 | if (!selection.has(item.get(_this4.state.idField, null))) {
537 | // Some data not in selection
538 | result = false;
539 | return false;
540 | }
541 | });
542 |
543 | return result;
544 | }
545 |
546 | /**
547 | * Set up the default selection if exist
548 | *
549 | * @param {array || string ... number} defSelection Default selection to be applied to the list
550 | */
551 |
552 | }, {
553 | key: 'setDefaultSelection',
554 | value: function setDefaultSelection(defSelection) {
555 | if (defSelection) {
556 | var selection = null;
557 |
558 | if (defSelection.length == 0) {
559 | selection = new Set();
560 | } else {
561 | if (!_underscore2['default'].isArray(defSelection)) {
562 | selection = new Set([defSelection.toString()]);
563 | } else {
564 | selection = new Set(defSelection.toString().split(','));
565 | }
566 | }
567 |
568 | selection['delete'](''); // Remove empty values
569 |
570 | this.triggerSelection(selection, false);
571 | }
572 | }
573 |
574 | /**
575 | * Prepare the data received by the component for the internal use.
576 | *
577 | * @param (object) newData New data for rebuild. (filtering || props changed)
578 | * @param (string) idField New idField if it has been changed. (props changed)
579 | * @param (boolean) rebuild Rebuild the data. NOTE: If newData its an Immutable you should put this param to true.
580 | *
581 | * @return (array) -rawdata: The same data as the props or the newData in case has been received.
582 | * -indexed: Same as rawdata but indexed by the idField
583 | * -data: Parsed data to add some fields necesary to internal working.
584 | */
585 |
586 | }, {
587 | key: 'prepareData',
588 | value: function prepareData() {
589 | var newData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
590 | var idField = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
591 | var rebuild = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
592 | var displayfield = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
593 |
594 | // The data will be inmutable inside the component
595 | var data = newData || this.props.data,
596 | index = 0,
597 | rdataIndex = 0,
598 | idSet = new Set(),
599 | field = idField || this.state.idField,
600 | fieldValue = undefined;
601 | var indexed = [],
602 | parsed = [],
603 | rawdata = undefined,
604 | hasNulls = false;
605 |
606 | // If not Immutable.
607 | // If an Immutable is received in props.data at the components first building the component will work with that data. In that case
608 | // the component should get indexed and rawdata in props. It's up to the developer if he / she wants to work with data from outside
609 | // but it's important to keep in mind that you need a similar data structure (_selected, _rowIndex, idField...)
610 | if (!_immutable2['default'].Iterable.isIterable(data) || rebuild) {
611 | data = _immutable2['default'].fromJS(data); // If data it's already Immutable the method .fromJS return the same object
612 |
613 | // Parsing data to add new fields (selected or not, field, rowIndex)
614 | parsed = data.map(function (row) {
615 | fieldValue = row.get(field, false);
616 |
617 | if (!fieldValue) {
618 | fieldValue = _underscore2['default'].uniqueId();
619 | }
620 |
621 | // No rows with same idField. The idField must be unique and also don't render the empty values
622 | if (!idSet.has(fieldValue) && fieldValue !== '' && row.get(displayfield, '') !== '') {
623 | idSet.add(fieldValue);
624 | row = row.set(field, fieldValue.toString());
625 |
626 | if (!row.get('_selected', false)) {
627 | row = row.set('_selected', false);
628 | }
629 |
630 | row = row.set('_rowIndex', index++); // data row index
631 | row = row.set('_rawDataIndex', rdataIndex++); // rawData row index
632 |
633 | return row;
634 | }
635 |
636 | rdataIndex++; // add 1 to jump over duplicate values
637 | hasNulls = true;
638 | return null;
639 | });
640 |
641 | // Clear null values if exist
642 | if (hasNulls) {
643 | parsed = parsed.filter(function (element) {
644 | return !_underscore2['default'].isNull(element);
645 | });
646 | }
647 |
648 | // Prepare indexed data.
649 | indexed = _underscore2['default'].indexBy(parsed.toJSON(), field);
650 | } else {
651 | // In case received Inmutable data, indexed data and raw data in props.
652 | data = this.props.rawdata;
653 | parsed = this.props.data;
654 | indexed = this.props.indexed;
655 | }
656 |
657 | return {
658 | rawdata: data,
659 | data: parsed,
660 | indexed: indexed
661 | };
662 | }
663 |
664 | /**
665 | * Function called each time the selection has changed. Apply an update in the components state selection then render again an update the child
666 | * list.
667 | *
668 | * @param (Set object) selection The selected values using the values of the selected data.
669 | * @param (Boolean) emptySelection When allowsEmptySelection is true and someone wants the empty selection.
670 | */
671 |
672 | }, {
673 | key: 'handleSelectionChange',
674 | value: function handleSelectionChange(selection) {
675 | var emptySelection = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
676 |
677 | if (!emptySelection) {
678 | this.triggerSelection(selection);
679 | } else {
680 | this.sendEmptySelection();
681 | }
682 | }
683 |
684 | /**
685 | * Function called each time the search field has changed. Filter the data by using the received search field value.
686 | *
687 | * @param (String) value String written in the search field
688 | */
689 |
690 | }, {
691 | key: 'handleSearch',
692 | value: function handleSearch(value) {
693 | var _this5 = this;
694 |
695 | var lValue = value ? value : null,
696 | filter = null;
697 | var data = this.state.initialData,
698 | filteredData = data,
699 | selection = this.state.selection;
700 | var displayField = this.state.displayField,
701 | idField = this.state.idField;
702 | var hasFilter = typeof this.props.filter == 'function';
703 |
704 | // When the search field has been clear then the value will be null and the data will be the same as initialData, otherwise
705 | // the data will be filtered using the .filter() function of Inmutable lib. It return a Inmutable obj with the elements that
706 | // match the expresion in the parameter.
707 | if (value) {
708 | lValue = _normalize2['default'].normalize(lValue);
709 |
710 | // If the prop `filter´ has a function then use if to filter as an interator over the indexed data.
711 | if (hasFilter) {
712 | (function () {
713 | var filtered = null,
714 | filteredIndexes = new Set();
715 |
716 | // Filter indexed data using the funtion
717 | _underscore2['default'].each(_this5.state.initialIndexed, function (element) {
718 | if (_this5.props.filter(element, lValue)) {
719 | filteredIndexes.add(element._rowIndex);
720 | }
721 | });
722 |
723 | // Then get the data that match with that indexed data
724 | filteredData = data.filter(function (element) {
725 | return filteredIndexes.has(element.get('_rowIndex'));
726 | });
727 | })();
728 | } else {
729 | filteredData = data.filter(function (element) {
730 | filter = element.get(_this5.props.filterField, null) || element.get(displayField);
731 |
732 | // When it's a function then use the field in filterField to search, if this field doesn't exist then use the field name or then idField.
733 | if (typeof filter == 'function') {
734 | filter = element.get('name', null) || element.get(idField);
735 | }
736 |
737 | filter = _normalize2['default'].normalize(filter);
738 | return filter.indexOf(lValue) >= 0;
739 | });
740 | }
741 | }
742 |
743 | // Apply selection
744 | filteredData = filteredData.map(function (element) {
745 | if (selection.has(element.get(idField, null))) {
746 | element = element.set('_selected', true);
747 | }
748 |
749 | return element;
750 | });
751 |
752 | this.setState({
753 | data: filteredData
754 | }, this.sendSearch(lValue));
755 | }
756 |
757 | /**
758 | * Get the data that match with the selection in params and send the data and the selection to a function whichs name is afterSelect
759 | * if this function was set up in the component props
760 | */
761 |
762 | }, {
763 | key: 'sendSelection',
764 | value: function sendSelection() {
765 | var _this6 = this;
766 |
767 | var hasAfterSelect = typeof this.props.afterSelect == 'function',
768 | hasGetSelection = typeof this.props.afterSelectGetSelection == 'function';
769 |
770 | if (hasAfterSelect || hasGetSelection) {
771 | (function () {
772 | var selectionArray = [],
773 | selection = _this6.state.selection;
774 |
775 | // Parse the selection to return it as an array instead of a Set obj
776 | selection.forEach(function (item) {
777 | selectionArray.push(item.toString());
778 | });
779 |
780 | if (hasGetSelection) {
781 | // When you just need the selection but no data
782 | _this6.props.afterSelectGetSelection.call(_this6, selectionArray, selection); // selection array / selection Set()
783 | }
784 |
785 | if (hasAfterSelect) {
786 | (function () {
787 | var selectedData = [],
788 | properId = null,
789 | rowIndex = null,
790 | filteredData = null;
791 | var _state = _this6.state;
792 | var indexedData = _state.indexedData;
793 | var initialData = _state.initialData;
794 | var rawData = _state.rawData;
795 | var data = _state.data;
796 |
797 | var fields = new Set(_underscore2['default'].keys(rawData.get(0).toJSON())),
798 | hasIdField = fields.has(_this6.state.idField) ? true : false;
799 |
800 | if (hasIdField) {
801 | selectedData = rawData.filter(function (element) {
802 | return selection.has(element.get(_this6.state.idField).toString());
803 | });
804 | } else {
805 | // Get the data (initialData) that match with the selection
806 | filteredData = initialData.filter(function (element) {
807 | return selection.has(element.get(_this6.state.idField));
808 | });
809 |
810 | // Then from the filtered data get the raw data that match with the selection
811 | selectedData = filteredData.map(function (row) {
812 | properId = row.get(_this6.state.idField);
813 | rowIndex = _this6.state.initialIndexed[properId]._rawDataIndex;
814 |
815 | return rawData.get(rowIndex);
816 | });
817 | }
818 |
819 | _this6.props.afterSelect.call(_this6, selectedData.toJSON(), selectionArray);
820 | })();
821 | }
822 | })();
823 | }
824 | }
825 | }, {
826 | key: 'sendEmptySelection',
827 | value: function sendEmptySelection() {
828 | var _this7 = this;
829 |
830 | var hasAfterSelect = typeof this.props.afterSelect == 'function',
831 | hasGetSelection = typeof this.props.afterSelectGetSelection == 'function';
832 |
833 | if (hasAfterSelect || hasGetSelection) {
834 | if (hasGetSelection) {
835 | // When you just need the selection but no data
836 | this.props.afterSelectGetSelection.call(this, [''], new Set(''));
837 | }
838 |
839 | if (hasAfterSelect) {
840 | (function () {
841 | var filteredData = null,
842 | rawData = _this7.state.rawData,
843 | id = undefined,
844 | display = undefined;
845 |
846 | // Get the data (rawData) that have idField or displayfield equals to empty string
847 | filteredData = rawData.filter(function (element) {
848 | id = element.get(_this7.state.idField);
849 | display = element.get(_this7.state.displayField);
850 | return display === '' || display === null || id === '' || id === null;
851 | });
852 |
853 | _this7.props.afterSelect.call(_this7, filteredData.toJSON(), ['']);
854 | })();
855 | }
856 | }
857 | }
858 |
859 | /**
860 | * Send the written string in the search field to the afterSearch function if it was set up in the components props
861 | *
862 | * @param (String) searchValue String written in the search field
863 | */
864 |
865 | }, {
866 | key: 'sendSearch',
867 | value: function sendSearch(searchValue) {
868 | if (typeof this.props.afterSearch == 'function') {
869 | this.props.afterSearch.call(this, searchValue);
870 | }
871 | }
872 | }, {
873 | key: 'render',
874 | value: function render() {
875 | var messages = this.getTranslatedMessages(),
876 | content = null,
877 | data = this.state.data,
878 | selection = new Set(),
879 | allSelected = this.state.allSelected,
880 | className = "proper-search";
881 |
882 | if (this.props.className) {
883 | className += ' ' + this.props.className;
884 | }
885 |
886 | if (this.state.ready) {
887 | this.state.selection.forEach(function (element) {
888 | selection.add(element);
889 | });
890 |
891 | content = _react2['default'].createElement(
892 | 'div',
893 | null,
894 | _react2['default'].createElement(_reactPropersearchField2['default'], {
895 | onSearch: this.handleSearch.bind(this),
896 | onEnter: this.props.onEnter,
897 | className: this.props.fieldClass,
898 | placeholder: this.props.placeholder,
899 | defaultValue: this.props.defaultSearch,
900 | searchIcon: this.props.searchIcon,
901 | clearIcon: this.props.clearIcon,
902 | throttle: this.props.throttle,
903 | minLength: this.props.minLength,
904 | autoComplete: this.props.autoComplete
905 | }),
906 | _react2['default'].createElement(_searchList2['default'], {
907 | data: data,
908 | rowFormater: this.props.rowFormater,
909 | indexedData: this.state.initialIndexed,
910 | className: this.props.listClass,
911 | idField: this.state.idField,
912 | displayField: this.state.displayField,
913 | onSelectionChange: this.handleSelectionChange.bind(this),
914 | messages: messages,
915 | selection: selection,
916 | allSelected: allSelected,
917 | multiSelect: this.props.multiSelect,
918 | listHeight: this.props.listHeight,
919 | listWidth: this.props.listWidth,
920 | listRowHeight: this.props.listRowHeight,
921 | listElementClass: this.props.listElementClass,
922 | showIcon: this.props.listShowIcon,
923 | cacheManager: this.props.cacheManager,
924 | allowsEmptySelection: this.props.allowsEmptySelection,
925 | hiddenSelection: this.props.hiddenSelection
926 | })
927 | );
928 | } else {
929 | content = _react2['default'].createElement(
930 | 'div',
931 | { className: 'proper-search-loading' },
932 | messages.loading
933 | );
934 | }
935 |
936 | return _react2['default'].createElement(
937 | 'div',
938 | { className: "proper-search " + className },
939 | content
940 | );
941 | }
942 | }]);
943 |
944 | return Search;
945 | }(_react2['default'].Component);
946 |
947 | ;
948 |
949 | Search.defaultProps = getDefaultProps();
950 | exports['default'] = Search;
951 | module.exports = exports['default'];
--------------------------------------------------------------------------------
/lib/components/searchList.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _underscore = require('underscore');
14 |
15 | var _underscore2 = _interopRequireDefault(_underscore);
16 |
17 | var _reactImmutableRenderMixin = require('react-immutable-render-mixin');
18 |
19 | var _reactVirtualized = require('react-virtualized');
20 |
21 | var _reactDimensions = require('react-dimensions');
22 |
23 | var _reactDimensions2 = _interopRequireDefault(_reactDimensions);
24 |
25 | var _cache = require('../lib/cache');
26 |
27 | var _cache2 = _interopRequireDefault(_cache);
28 |
29 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
30 |
31 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
32 |
33 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
34 |
35 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
36 |
37 | var Set = require('es6-set');
38 |
39 | // For more info about this read ReadMe.md
40 | function getDefaultProps() {
41 | return {
42 | data: null,
43 | indexedData: null, // Just when you use a function as a display field. (array) (Full indexed data not filted)
44 | onSelectionChange: null,
45 | rowFormater: null, // function
46 | multiSelect: false,
47 | messages: null,
48 | selection: new Set(),
49 | allSelected: false,
50 | listRowHeight: 26,
51 | listHeight: 200,
52 | listWidth: 100, // Container width by default
53 | idField: 'value',
54 | displayField: 'label',
55 | showIcon: true,
56 | listElementClass: null,
57 | allowsEmptySelection: false,
58 | hiddenSelection: null,
59 | uniqueID: null
60 | };
61 | }
62 |
63 | /**
64 | * A Component that render a list of selectable items, with single or multiple selection and return the selected items each time a new item be selected.
65 | *
66 | * Simple example usage:
67 | * let handleSelection = function(selection){
68 | * console.log('Selected values: ' + selection) // The selection is a Set obj
69 | * }
70 | *
71 | *
77 | * ```
78 | */
79 |
80 | var SearchList = function (_React$Component) {
81 | _inherits(SearchList, _React$Component);
82 |
83 | function SearchList(props) {
84 | _classCallCheck(this, SearchList);
85 |
86 | var _this = _possibleConstructorReturn(this, (SearchList.__proto__ || Object.getPrototypeOf(SearchList)).call(this, props));
87 |
88 | _this.state = {
89 | allSelected: props.allSelected,
90 | nothingSelected: props.selection.size == 0,
91 | hiddenSelection: new Set()
92 | };
93 | return _this;
94 | }
95 |
96 | _createClass(SearchList, [{
97 | key: 'componentWillMount',
98 | value: function componentWillMount() {
99 | this.uniqueId = this.props.uniqueID ? this.props.uniqueID : _underscore2['default'].uniqueId('search_list_');
100 | if (this.props.hiddenSelection) {
101 | this.setState({
102 | hiddenSelection: this.parseHiddenSelection(this.props)
103 | });
104 | }
105 | }
106 | }, {
107 | key: 'shouldComponentUpdate',
108 | value: function shouldComponentUpdate(nextProps, nextState) {
109 | var propschanged = !(0, _reactImmutableRenderMixin.shallowEqualImmutable)(this.props, nextProps);
110 | var stateChanged = !(0, _reactImmutableRenderMixin.shallowEqualImmutable)(this.state, nextState);
111 | var somethingChanged = propschanged || stateChanged;
112 |
113 | if (propschanged) {
114 | var nothingSelected = false;
115 |
116 | if (!nextProps.allSelected) nothingSelected = this.isNothingSelected(nextProps.data, nextProps.selection);
117 |
118 | // When the props change update the state.
119 | if (nextProps.allSelected != this.state.allSelected || nothingSelected != this.state.nothingSelected) {
120 | this.setState({
121 | allSelected: nextProps.allSelected,
122 | nothingSelected: nothingSelected
123 | });
124 | }
125 | }
126 |
127 | return somethingChanged;
128 | }
129 | }, {
130 | key: 'componentWillReceiveProps',
131 | value: function componentWillReceiveProps(newProps) {
132 | var hiddenChange = !(0, _reactImmutableRenderMixin.shallowEqualImmutable)(this.props.hiddenSelection, newProps.hiddenSelection);
133 | var hiddenSelection = undefined;
134 |
135 | if (hiddenChange) {
136 | if (this._virtualScroll) this._virtualScroll.recomputeRowHeights(0);
137 | hiddenSelection = this.parseHiddenSelection(newProps);
138 | this.setState({
139 | hiddenSelection: hiddenSelection
140 | });
141 | }
142 | }
143 |
144 | /**
145 | * Function called each time an element of the list is selected. Get the value (value of the idField) of the
146 | * element that was selected, them change the selection and call to onSelectionChange function in the props sending
147 | * the new selection.
148 | *
149 | * @param (String) itemValue Value of the idField of the selected element
150 | * @param (Array) e Element which call the function
151 | */
152 |
153 | }, {
154 | key: 'handleElementClick',
155 | value: function handleElementClick(itemValue, e) {
156 | e.preventDefault();
157 | var data = this.props.data,
158 | selection = this.props.selection,
159 | nothingSelected = false,
160 | allSelected = false;
161 |
162 | if (this.props.multiSelect) {
163 | if (selection.has(itemValue)) {
164 | selection['delete'](itemValue);
165 | } else {
166 | selection.add(itemValue);
167 | }
168 | } else {
169 | if (selection.has(itemValue)) selection = new Set();else selection = new Set([itemValue]);
170 | }
171 |
172 | allSelected = this.isAllSelected(data, selection);
173 | if (!allSelected) nothingSelected = this.isNothingSelected(data, selection);
174 |
175 | // If the state has changed update it
176 | if (allSelected != this.state.allSelected || nothingSelected != this.state.nothingSelected) {
177 | this.setState({
178 | allSelected: allSelected,
179 | nothingSelected: nothingSelected
180 | });
181 | }
182 |
183 | if (typeof this.props.onSelectionChange == 'function') {
184 | this.props.onSelectionChange.call(this, selection);
185 | }
186 | }
187 |
188 | /**
189 | * Check if all the current data are not selected
190 | *
191 | * @param (array) data The data to compare with selection
192 | * @param (object) selection The current selection Set of values (idField)
193 | */
194 |
195 | }, {
196 | key: 'isNothingSelected',
197 | value: function isNothingSelected(data, selection) {
198 | var _this2 = this;
199 |
200 | var result = true;
201 | if (selection.size == 0) return true;
202 |
203 | data.forEach(function (element) {
204 | if (selection.has(element.get(_this2.props.idField, null))) {
205 | // Some data not in selection
206 | result = false;
207 | return false;
208 | }
209 | });
210 |
211 | return result;
212 | }
213 |
214 | /**
215 | * Check if all the current data are selected.
216 | *
217 | * @param (array) data The data to compare with selection
218 | * @param (object) selection The current selection Set of values (idField)
219 | */
220 |
221 | }, {
222 | key: 'isAllSelected',
223 | value: function isAllSelected(data, selection) {
224 | var _this3 = this;
225 |
226 | var result = true;
227 | if (data.size > selection.size) return false;
228 |
229 | data.forEach(function (element) {
230 | if (!selection.has(element.get(_this3.props.idField, null))) {
231 | // Some data not in selection
232 | result = false;
233 | return false;
234 | }
235 | });
236 |
237 | return result;
238 | }
239 |
240 | /**
241 | * Function called each time the buttons in the bar of the list has been clicked. Delete or add all the data elements into the selection, just if it has changed.
242 | *
243 | * @param (Boolean) selectAll If its a select all action or an unselect all.
244 | * @param (Array) e Element which call the function
245 | */
246 |
247 | }, {
248 | key: 'handleSelectAll',
249 | value: function handleSelectAll(selectAll, e) {
250 | e.preventDefault();
251 |
252 | var newData = [],
253 | data = this.props.data,
254 | field = this.props.idField;
255 | var selection = this.props.selection;
256 | var hasChanged = selectAll != this.state.allSelected || !selectAll && !this.state.nothingSelected; // nothingSelected = false then something is selected
257 |
258 | if (selectAll && hasChanged) {
259 | data.forEach(function (element) {
260 | selection.add(element.get(field, null));
261 | });
262 | } else {
263 | data.forEach(function (element) {
264 | selection['delete'](element.get(field, null));
265 | });
266 | }
267 |
268 | if (hasChanged) {
269 | this.setState({
270 | allSelected: selectAll,
271 | nothingSelected: !selectAll
272 | });
273 | }
274 |
275 | if (typeof this.props.onSelectionChange == 'function' && hasChanged) {
276 | this.props.onSelectionChange.call(this, selection);
277 | }
278 | }
279 |
280 | /**
281 | * Function called each time the buttons (select empty) in the bar of the list has been clicked (In case empty selection allowed).
282 | *
283 | * @param (Array) e Element which call the function
284 | */
285 |
286 | }, {
287 | key: 'handleSelectEmpty',
288 | value: function handleSelectEmpty(e) {
289 | if (typeof this.props.onSelectionChange == 'function') {
290 | this.props.onSelectionChange.call(this, null, true);
291 | }
292 | }
293 |
294 | /**
295 | * Parse the hidden selection if that property contains somethings.
296 | *
297 | * @param (array) props Component props (or nextProps)
298 | * @return (Set) hiddenSelection The hidden rows.
299 | */
300 |
301 | }, {
302 | key: 'parseHiddenSelection',
303 | value: function parseHiddenSelection() {
304 | var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
305 |
306 | var hidden = [],
307 | isArray = _underscore2['default'].isArray(props.hiddenSelection),
308 | isObject = _underscore2['default'].isObject(props.hiddenSelection);
309 |
310 | if (!isArray && isObject) return props.hiddenSelection; // Is Set
311 |
312 | if (!isArray) {
313 | // Is String or number
314 | hidden = [props.hiddenSelection.toString()];
315 | } else if (props.hiddenSelection.length > 0) {
316 | // Is Array
317 | hidden = props.hiddenSelection.toString().split(',');
318 | }
319 |
320 | return new Set(hidden);
321 | }
322 |
323 | /**
324 | * Return the tool bar for the top of the list. It will be displayed only when the selection can be multiple.
325 | *
326 | * @return (html) The toolbar code
327 | */
328 |
329 | }, {
330 | key: 'getToolbar',
331 | value: function getToolbar() {
332 | var maxWidth = this.props.containerWidth ? this.props.containerWidth / 2 - 1 : 100;
333 |
334 | return _react2['default'].createElement(
335 | 'div',
336 | { className: 'proper-search-list-bar' },
337 | _react2['default'].createElement(
338 | 'div',
339 | { className: 'btn-group form-inline' },
340 | _react2['default'].createElement(
341 | 'a',
342 | {
343 | id: 'proper-search-list-bar-check',
344 | className: 'btn-select list-bar-check', role: 'button',
345 | onClick: this.handleSelectAll.bind(this, true),
346 | style: { maxWidth: maxWidth, boxSizing: 'border-box' } },
347 | _react2['default'].createElement(
348 | 'label',
349 | null,
350 | this.props.messages.all
351 | )
352 | ),
353 | _react2['default'].createElement(
354 | 'a',
355 | {
356 | id: 'proper-search-list-bar-unCheck',
357 | className: 'btn-select list-bar-unCheck',
358 | role: 'button',
359 | onClick: this.handleSelectAll.bind(this, false),
360 | style: { maxWidth: maxWidth, boxSizing: 'border-box' } },
361 | _react2['default'].createElement(
362 | 'label',
363 | null,
364 | this.props.messages.none
365 | )
366 | )
367 | )
368 | );
369 | }
370 |
371 | /**
372 | * Return the tool bar for the top of the list in case Empty Selection allowed
373 | *
374 | * @return (html) The toolbar code
375 | */
376 |
377 | }, {
378 | key: 'getToolbarForEmpty',
379 | value: function getToolbarForEmpty() {
380 | var allSelected = this.state.allSelected,
381 | selectMessage = undefined,
382 | maxWidth = this.props.containerWidth / 2 - 1;
383 | selectMessage = allSelected ? this.props.messages.none : this.props.messages.all;
384 |
385 | return _react2['default'].createElement(
386 | 'div',
387 | { className: 'proper-search-list-bar' },
388 | _react2['default'].createElement(
389 | 'div',
390 | { className: 'btn-group form-inline' },
391 | _react2['default'].createElement(
392 | 'a',
393 | {
394 | id: 'proper-search-list-bar-select',
395 | className: 'btn-select list-bar-select',
396 | role: 'button',
397 | onClick: this.handleSelectAll.bind(this, !allSelected),
398 | style: { maxWidth: maxWidth, boxSizing: 'border-box' } },
399 | _react2['default'].createElement(
400 | 'label',
401 | null,
402 | selectMessage
403 | )
404 | ),
405 | _react2['default'].createElement(
406 | 'a',
407 | {
408 | id: 'proper-search-list-bar-empty',
409 | className: 'btn-select list-bar-empty',
410 | role: 'button',
411 | onClick: this.handleSelectEmpty.bind(this),
412 | style: { maxWidth: maxWidth, boxSizing: 'border-box' } },
413 | _react2['default'].createElement(
414 | 'label',
415 | null,
416 | this.props.messages.empty
417 | )
418 | )
419 | )
420 | );
421 | }
422 |
423 | /**
424 | * Build and return the content of the list.
425 | *
426 | * @param (object) contentData
427 | * - index (integer) Index of the data to be rendered
428 | * - isScrolling (bool) If grid is scrollings
429 | * @return (html) list-row A row of the list
430 | */
431 |
432 | }, {
433 | key: 'getContent',
434 | value: function getContent(contentData) {
435 | var index = contentData.index;
436 | var icon = null,
437 | selectedClass = null,
438 | className = null,
439 | element = null,
440 | listElementClass = this.props.listElementClass;
441 | var data = this.props.data,
442 | rowdata = undefined,
443 | id = undefined,
444 | displayField = this.props.displayField,
445 | showIcon = this.props.showIcon;
446 |
447 | rowdata = data.get(index);
448 | element = rowdata.get(displayField);
449 | className = "proper-search-list-element";
450 | id = rowdata.get(this.props.idField);
451 |
452 | if (this.props.multiSelect) {
453 | if (showIcon) {
454 | if (rowdata.get('_selected', false)) {
455 | icon = _react2['default'].createElement('i', { className: 'fa fa-check-square-o' });
456 | selectedClass = ' proper-search-selected';
457 | } else {
458 | icon = _react2['default'].createElement('i', { className: 'fa fa-square-o' });
459 | selectedClass = null;
460 | }
461 | }
462 | } else {
463 | if (rowdata.get('_selected')) selectedClass = ' proper-search-single-selected';else selectedClass = null;
464 | }
465 |
466 | if (listElementClass) {
467 | className += ' ' + listElementClass;
468 | }
469 |
470 | if (selectedClass) {
471 | className += ' ' + selectedClass;
472 | }
473 |
474 | if (typeof element == 'function') {
475 | element = element(this.props.indexedData[id]);
476 | } else if (this.props.rowFormater) {
477 | var ckey = ['search_list', 'list_' + this.uniqueId, 'row__' + rowdata.get(this.props.idField), displayField];
478 | element = _cache2['default'].read(ckey);
479 |
480 | if (element === undefined) {
481 | element = this.props.rowFormater(rowdata.get(displayField));
482 | _cache2['default'].write(ckey, element);
483 | }
484 | }
485 |
486 | return _react2['default'].createElement(
487 | 'div',
488 | { key: 'element-' + index, className: className, onClick: this.handleElementClick.bind(this, id) },
489 | icon,
490 | element
491 | );
492 | }
493 | /**
494 | * To be rendered when the data has no data (Ex. filtered data)
495 | *
496 | * @return (node) An div with a message
497 | */
498 |
499 | }, {
500 | key: 'noRowsRenderer',
501 | value: function noRowsRenderer() {
502 | return _react2['default'].createElement(
503 | 'div',
504 | { key: 'element-0', className: "proper-search-list search-list-no-data" },
505 | this.props.messages.noData
506 | );
507 | }
508 |
509 | /**
510 | * Function called to get the content of each element of the list.
511 | *
512 | * @param (object) contentData
513 | * - index (integer) Index of the data to be rendered
514 | * - isScrolling (bool) If grid is scrollings
515 | * @return (node) element The element on the index position
516 | */
517 |
518 | }, {
519 | key: 'rowRenderer',
520 | value: function rowRenderer(contentData) {
521 | return this.getContent(contentData);
522 | }
523 |
524 | /**
525 | * Function that gets the height for the current row of the list.
526 | *
527 | * @param (object) rowData It's an object that contains the index of the current row
528 | * @return (integer) rowHeight The height of each row.
529 | */
530 |
531 | }, {
532 | key: 'getRowHeight',
533 | value: function getRowHeight(rowData) {
534 | var id = this.props.data.get(rowData.index).get(this.props.idField);
535 | return this.state.hiddenSelection.has(id) ? 0 : this.props.listRowHeight;
536 | }
537 | }, {
538 | key: 'render',
539 | value: function render() {
540 | var _this4 = this;
541 |
542 | var toolbar = null,
543 | rowHeight = this.props.listRowHeight,
544 | className = "proper-search-list";
545 |
546 | if (this.props.multiSelect) {
547 | toolbar = this.props.allowsEmptySelection ? this.getToolbarForEmpty() : this.getToolbar();
548 | }
549 |
550 | if (this.props.className) {
551 | className += ' ' + this.props.className;
552 | }
553 |
554 | if (this.state.hiddenSelection.size > 0) {
555 | rowHeight = this.getRowHeight.bind(this);
556 | }
557 |
558 | return _react2['default'].createElement(
559 | 'div',
560 | { className: className },
561 | toolbar,
562 | _react2['default'].createElement(_reactVirtualized.VirtualScroll, {
563 | ref: function ref(_ref) {
564 | _this4._virtualScroll = _ref;
565 | },
566 | className: "proper-search-list-virtual",
567 | width: this.props.listWidth || this.props.containerWidth,
568 | height: this.props.listHeight,
569 | rowRenderer: this.rowRenderer.bind(this),
570 | rowHeight: rowHeight,
571 | noRowsRenderer: this.noRowsRenderer.bind(this),
572 | rowCount: this.props.data.size,
573 | overscanRowsCount: 5
574 | })
575 | );
576 | }
577 | }]);
578 |
579 | return SearchList;
580 | }(_react2['default'].Component);
581 |
582 | SearchList.defaultProps = getDefaultProps();
583 | var toExport = process.env.NODE_ENV === 'Test' ? SearchList : (0, _reactDimensions2['default'])()(SearchList);
584 | exports['default'] = toExport;
585 | module.exports = exports['default'];
--------------------------------------------------------------------------------
/lib/lang/messages.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports['default'] = {
7 | 'SPA': {
8 | all: 'Seleccionar Todo',
9 | none: 'Deseleccionar Todo',
10 | empty: 'Seleccionar Vacios',
11 | notEmpty: 'Deseleccionar Vacios',
12 | loading: 'Cargando...',
13 | noData: 'No se encontró ningún elemento',
14 | errorIdField: 'No se pudo cambiar el `idField´, el campo',
15 | errorDisplayField: 'No se pudo cambiar el `displayField´, el campo',
16 | errorData: 'no existe en el array de datos o no ha cambiado'
17 | },
18 | 'ENG': {
19 | all: 'Select All',
20 | none: 'Unselect All',
21 | empty: 'Select Empty',
22 | notEmpty: 'Unselect Empty',
23 | loading: 'Loading...',
24 | noData: 'No data found',
25 | errorIdField: "Couldn\'t change the `idField´, the field",
26 | errorDisplayField: "Couldn\'t change the `displayField´, the field",
27 | errorData: 'doesn\'t exist in the data array or has no changes'
28 | }
29 | };
30 | module.exports = exports['default'];
--------------------------------------------------------------------------------
/lib/lib/cache.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _dotObject = require('dot-object');
10 |
11 | var _dotObject2 = _interopRequireDefault(_dotObject);
12 |
13 | var _underscore = require('underscore');
14 |
15 | var _deepmerge = require('deepmerge');
16 |
17 | var _deepmerge2 = _interopRequireDefault(_deepmerge);
18 |
19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
20 |
21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
22 |
23 | var cache = {};
24 |
25 | function parseKey(key) {
26 | return (0, _underscore.map)(key, function (k) {
27 | return k.toString().replace('.', '_');
28 | }).join('.');
29 | }
30 |
31 | var RowCache = function () {
32 | function RowCache() {
33 | var base = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
34 |
35 | _classCallCheck(this, RowCache);
36 |
37 | this.init(base);
38 | }
39 |
40 | _createClass(RowCache, [{
41 | key: 'init',
42 | value: function init() {
43 | var base = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
44 |
45 | cache = base;
46 |
47 | return this;
48 | }
49 | }, {
50 | key: 'read',
51 | value: function read(key) {
52 | var k = parseKey(key);
53 | return _dotObject2['default'].pick(k, cache);
54 | }
55 | }, {
56 | key: 'write',
57 | value: function write(key, value) {
58 | var k = parseKey(key);
59 | var writable = {};
60 |
61 | writable[k] = value;
62 | writable = _dotObject2['default'].object(writable);
63 |
64 | cache = (0, _deepmerge2['default'])(cache, writable);
65 |
66 | return this;
67 | }
68 | }, {
69 | key: 'flush',
70 | value: function flush() {
71 | var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
72 |
73 | if (key) {
74 | var k = parseKey(key);
75 | _dotObject2['default'].remove(k, cache);
76 | } else {
77 | this.init();
78 | }
79 |
80 | return this;
81 | }
82 | }]);
83 |
84 | return RowCache;
85 | }();
86 |
87 | var rowcache = new RowCache();
88 |
89 | exports['default'] = rowcache;
90 | module.exports = exports['default'];
--------------------------------------------------------------------------------
/lib/propersearch.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _search = require("./components/search");
8 |
9 | var _search2 = _interopRequireDefault(_search);
10 |
11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
12 |
13 | if (process.env.APP_ENV === 'browser') {
14 | require("../css/style.scss");
15 | }
16 |
17 | exports["default"] = _search2["default"];
18 | module.exports = exports['default'];
--------------------------------------------------------------------------------
/lib/utils/normalize.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | var charMap = {
7 | 'a': ['á', 'Á', 'à', 'À', 'ã', 'Ã', 'â', 'Â', 'ä', 'Ä', 'å', 'Å', 'ā', 'Ā', 'ą', 'Ą'],
8 | 'e': ['é', 'É', 'è', 'È', 'ê', 'Ê', 'ë', 'Ë', 'ē', 'Ē', 'ė', 'Ė', 'ę', 'Ę'],
9 | 'i': ['î', 'Î', 'í', 'Í', 'ì', 'Ì', 'ï', 'Ï', 'ī', 'Ī', 'į', 'Į'],
10 | 'l': ['ł', 'Ł'],
11 | 'o': ['ô', 'Ô', 'ò', 'Ò', 'ø', 'Ø', 'ō', 'Ō', 'ó', 'Ó', 'õ', 'Õ', 'ö', 'Ö'],
12 | 'u': ['û', 'Û', 'ú', 'Ú', 'ù', 'Ù', 'ü', 'Ü', 'ū', 'Ū'],
13 | 'c': ['ç', 'Ç', 'č', 'Č', 'ć', 'Ć'],
14 | 's': ['ś', 'Ś', 'š', 'Š'],
15 | 'z': ['ź', 'Ź', 'ż', 'Ż'],
16 | '': ['@', '#', '~', '$', '!', 'º', '|', '"', '·', '%', '&', '¬', '/', '(', ')', '=', '?', '¿', '¡', '*', '+', '^', '`', '-', '´', '{', '}', 'ç', ';', ':', '.']
17 | };
18 |
19 | exports['default'] = {
20 | normalize: function normalize(value) {
21 | var parseToLower = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
22 |
23 | var rex = null;
24 |
25 | for (var element in charMap) {
26 | rex = new RegExp('[' + charMap[element].toString() + ']', 'g');
27 |
28 | try {
29 | value = value.replace(rex, element);
30 | } catch (e) {
31 | console.log('error', value);
32 | }
33 | }
34 | return parseToLower ? value.toLowerCase() : value;
35 | }
36 | };
37 | module.exports = exports['default'];
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-propersearch",
3 | "version": "0.2.15",
4 | "description": "A react component that render a search field with a list of items, allows the user to filter that list and select the items. The component return the selected data when it get selected",
5 | "main": "lib/propersearch.js",
6 | "scripts": {
7 | "test": "webpack && karma start --single-run --no-auto-watch karma.conf.js",
8 | "clean": "rm -Rvf css && rm -Rvf dist && rm -Rvf lib && mkdir dist && mkdir css && mkdir lib",
9 | "watch": "karma start --auto-watch --no-single-run karma.conf.js",
10 | "start": "webpack-dev-server --hot --inline --config webpack.config.example.js",
11 | "build": "babel src/jsx --out-dir lib && webpack && cp -rvf src/css/* css/",
12 | "release": "npm run build && webpack --config webpack.config.min.js",
13 | "build-example": "webpack --config webpack.config.example.dist.js"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/CBIConsulting/ProperSearch.git"
18 | },
19 | "keywords": [
20 | "react",
21 | "react-component",
22 | "search",
23 | "filter",
24 | "ui"
25 | ],
26 | "author": "cbi",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/CBIConsulting/ProperSearch/issues"
30 | },
31 | "homepage": "https://github.com/CBIConsulting/ProperSearch#readme",
32 | "devDependencies": {
33 | "autoprefixer-loader": "^2.0.0",
34 | "babel-core": "^6.7.4",
35 | "babel-loader": "^6.2.4",
36 | "babel-plugin-add-module-exports": "^0.1.2",
37 | "babel-plugin-transform-es3-member-expression-literals": "^6.5.0",
38 | "babel-plugin-transform-es3-property-literals": "^6.5.0",
39 | "babel-preset-es2015": "^6.6.0",
40 | "babel-preset-react": "^6.5.0",
41 | "compass-mixins": "^0.12.7",
42 | "core-js": "^2.2.1",
43 | "css-loader": "^0.23.1",
44 | "eslint": "^2.5.3",
45 | "eslint-plugin-react": "^4.2.3",
46 | "extract-text-webpack-plugin": "^1.0.1",
47 | "file-loader": "^0.8.5",
48 | "imports-loader": "^0.6.5",
49 | "jasmine": "^2.4.1",
50 | "jasmine-core": "^2.4.1",
51 | "jquery": "^3.0.0",
52 | "karma": "^0.13.22",
53 | "karma-chrome-launcher": "^0.2.3",
54 | "karma-cli": "^0.1.2",
55 | "karma-coverage": "^0.5.5",
56 | "karma-jasmine": "^0.3.8",
57 | "karma-notification-reporter": "0.0.2",
58 | "karma-phantomjs-launcher": "^1.0.0",
59 | "karma-sourcemap-loader": "^0.3.7",
60 | "karma-webpack": "^1.7.0",
61 | "node-sass": "^3.4.2",
62 | "phantomjs-prebuilt": "^2.1.7",
63 | "react-addons-test-utils": "^0.14.7",
64 | "sass-loader": "^3.2.0",
65 | "style-loader": "^0.13.1",
66 | "webpack": "^1.13.1",
67 | "webpack-dev-server": "^1.14.1"
68 | },
69 | "dependencies": {
70 | "es6-set": "^0.1.4",
71 | "immutable": "^3.7.6",
72 | "react": "^0.14.7",
73 | "deepmerge": "^0.2.10",
74 | "dot-object": "^1.4.1",
75 | "react-propersearch-field": "^0.1.0",
76 | "react-dimensions": "~1.0.2",
77 | "react-dom": "^0.14.7",
78 | "react-addons-shallow-compare": "^0.14.7",
79 | "react-immutable-render-mixin": "^0.9.5",
80 | "react-virtualized": "^7.12.3",
81 | "underscore": "^1.8.3"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/css/_react-virtualized-styles.scss:
--------------------------------------------------------------------------------
1 | /* Grid default theme */
2 |
3 | .Grid {
4 | position: relative;
5 | overflow: auto;
6 | -webkit-overflow-scrolling: touch;
7 |
8 | /* Without this property, Chrome repaints the entire Grid any time a new row or column is added.
9 | Firefox only repaints the new row or column (regardless of this property).
10 | Safari and IE don't support the property at all. */
11 | will-change: transform;
12 | }
13 |
14 | .Grid__innerScrollContainer {
15 | box-sizing: border-box;
16 | overflow: hidden;
17 | }
18 |
19 | .Grid__cell {
20 | position: absolute;
21 | }
22 |
23 | /* FlexTable default theme */
24 |
25 | .FlexTable {
26 | }
27 |
28 | .FlexTable__Grid {
29 | overflow-x: hidden;
30 | }
31 |
32 | .FlexTable__headerRow {
33 | font-weight: 700;
34 | text-transform: uppercase;
35 | display: flex;
36 | flex-direction: row;
37 | align-items: center;
38 | overflow: hidden;
39 | }
40 | .FlexTable__headerTruncatedText {
41 | white-space: nowrap;
42 | text-overflow: ellipsis;
43 | overflow: hidden;
44 | }
45 | .FlexTable__row {
46 | display: flex;
47 | flex-direction: row;
48 | align-items: center;
49 | overflow: hidden;
50 | }
51 |
52 | .FlexTable__headerColumn,
53 | .FlexTable__rowColumn {
54 | margin-right: 10px;
55 | min-width: 0px;
56 | }
57 |
58 | .FlexTable__headerColumn:first-of-type,
59 | .FlexTable__rowColumn:first-of-type {
60 | margin-left: 10px;
61 | }
62 | .FlexTable__headerColumn {
63 | align-items: center;
64 | display: flex;
65 | flex-direction: row;
66 | overflow: hidden;
67 | }
68 | .FlexTable__sortableHeaderColumn {
69 | cursor: pointer;
70 | }
71 | .FlexTable__rowColumn {
72 | justify-content: center;
73 | flex-direction: column;
74 | display: flex;
75 | overflow: hidden;
76 | height: 100%;
77 | }
78 |
79 | .FlexTable__sortableHeaderIconContainer {
80 | display: flex;
81 | align-items: center;
82 | }
83 | .FlexTable__sortableHeaderIcon {
84 | flex: 0 0 24px;
85 | height: 1em;
86 | width: 1em;
87 | fill: currentColor;
88 | }
89 |
90 | .FlexTable__truncatedColumnText {
91 | text-overflow: ellipsis;
92 | overflow: hidden;
93 | }
94 |
95 | /* VirtualScroll default theme */
96 |
97 | .VirtualScroll {
98 | position: relative;
99 | overflow-y: auto;
100 | overflow-x: hidden;
101 | -webkit-overflow-scrolling: touch;
102 | }
103 |
104 | .VirtualScroll__innerScrollContainer {
105 | box-sizing: border-box;
106 | overflow: hidden;
107 | }
108 |
109 | .VirtualScroll__row {
110 | position: absolute;
111 | }
--------------------------------------------------------------------------------
/src/css/style.scss:
--------------------------------------------------------------------------------
1 | @import "compass";
2 | @import "react-virtualized-styles";
3 |
4 | .proper-search {
5 | background-color: #fff;
6 |
7 | .proper-search-field {
8 | font-size: 14px;
9 | margin: 0;
10 | line-height: 22px;
11 | border: none;
12 |
13 | .proper-search-input {
14 | background: #fff;
15 | padding: 3px 2px;
16 | background-color: #ccc;
17 | @include border-radius(3px 2px, 2px 3px);
18 |
19 | input[type="text"] {
20 | width: 100%;
21 | height: 1.8em;
22 | margin-bottom: 0;
23 | color: #777777;
24 | padding-left: 30px;
25 | padding-right: 30px;
26 | border: none;
27 | outline: none;
28 | box-shadow: none;
29 | webkit-box-shadow: none;
30 | }
31 |
32 | i.proper-search-field-icon {
33 | color: #777777;
34 | overflow: visible;
35 | margin-right: -20px;
36 | position: absolute;
37 | border: 0;
38 | padding: 4px;
39 | }
40 |
41 | .btn-clear {
42 | i {
43 | color: #777777;
44 | }
45 | background: none;
46 | overflow: visible;
47 | position: absolute;
48 | border: 0;
49 | padding-top: 0.2em;
50 | margin-left: -26px;
51 | outline: none;
52 |
53 | &:active, &:focus {
54 | box-shadow: none;
55 | webkit-box-shadow: none;
56 | }
57 | }
58 | }
59 | }
60 |
61 | .proper-search-list {
62 | .proper-search-list-bar {
63 | background-color: #f6f7f8;
64 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjEuMCIgeDI9IjAuNSIgeTI9IjAuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
65 | background-size: 100%;
66 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, rgba(230, 230, 230, 0.61)), color-stop(100%, #ffffff));
67 | background-image: -moz-linear-gradient(bottom, rgba(230,230,230,0.61), #ffffff);
68 | background-image: -webkit-linear-gradient(#fff,#efefef);
69 | background-image: linear-gradient(#fff,#efefef);
70 | padding: 1px 0;
71 | width: 100%;
72 | border: 0;
73 | line-height: 5px;
74 |
75 | .btn-group {
76 | width: 100%;
77 | }
78 |
79 | .btn-group label {
80 | position: relative;
81 | cursor: pointer;
82 | margin: 0;
83 | font-weight: 100;
84 | font-family: Helvetica;
85 | font-size: x-small;
86 | }
87 |
88 | .btn-group .btn-select {
89 | display: inline-block;
90 | line-height: 1.4;
91 | text-align: center;
92 | width: 100%;
93 | cursor: pointer !important;
94 | border: none;
95 | margin: 0;
96 | padding: 5px 8px 5px 8px !important;
97 | border-radius: 0;
98 | color: #333;
99 | }
100 |
101 | .btn-group .btn-select:hover,
102 | .btn-group .btn-select:focus,
103 | .btn-group .btn-select:active,
104 | .btn-group .btn-select.disabled,
105 | .btn-group .btn-select[disabled] {
106 | background-color: #CECECE;
107 | text-decoration: none;
108 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #CECECE), color-stop(100%, #ffffff));
109 | background-image: -moz-linear-gradient(bottom, #CECECE, #ffffff);
110 | background-image: -webkit-linear-gradient(#fff,#CECECE);
111 | background-image: linear-gradient(#fff,#CECECE);
112 | }
113 | }
114 |
115 | .proper-search-list-virtual {
116 | &:focus,&:active {
117 | border: none;
118 | outline: none;
119 | }
120 |
121 | .proper-search-list-element {
122 | height: inherit;
123 | width: inherit;
124 | padding-left: 0.3em;
125 | margin-bottom: 0.1em;
126 | padding-top: 0.1em;
127 | cursor: pointer;
128 | white-space: nowrap;
129 | overflow: hidden;
130 | text-overflow: ellipsis;
131 | box-sizing: border-box;
132 |
133 | &.proper-search-selected {
134 | i {
135 | color: #10AB10;
136 | }
137 | }
138 |
139 | &.proper-search-single-selected {
140 | background-color: #C7EBFF;
141 | }
142 |
143 | i {
144 | width: 14.5px;
145 | margin-right: 8px;
146 | }
147 | }
148 | }
149 | }
150 |
151 | .proper-search-loading {
152 | text-align: center;
153 | font-size: 14px;
154 | margin: auto;
155 | }
156 | }
--------------------------------------------------------------------------------
/src/jsx/components/__tests__/search-test.js:
--------------------------------------------------------------------------------
1 | import Search from '../search';
2 | import TestUtils from "react-addons-test-utils";
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import {Deferred} from 'jquery';
6 | const Set = require('es6-set');
7 |
8 | describe('Search', () => {
9 | let wrapper = null;
10 |
11 | beforeEach(function() {
12 | wrapper = document.createElement('div');
13 | });
14 |
15 | it('is available', () => {
16 | expect(Search).not.toBe(null);
17 | });
18 |
19 | it('filter', (done) => {
20 | let def = Deferred(), component = null, node = null, props = getProps();
21 | props.afterSearch = (searchValue) => {
22 | if (searchValue) def.resolve(searchValue);
23 | }
24 |
25 | component = prepare(props);
26 |
27 | // Check filter
28 | component.handleSearch('foo');
29 |
30 | def.done((value) => {
31 | expect(component.state.data.toJSON()[0].itemID).toBe('3'); // Testing filter
32 | expect(value).toBe('foo');
33 | }).always(done);
34 | });
35 |
36 | it('selection multiselect', (done) => {
37 | let def = Deferred(), component = null, props = getProps();
38 | props.afterSelect = (data, selection) => {
39 | def.resolve(data, selection);
40 | }
41 | component = prepare(props);
42 |
43 | // Check selection
44 | component.triggerSelection(new Set(['1','2']));
45 |
46 | def.done((data, selection) => {
47 | expect(data.length).toBe(2);
48 | expect(selection.length).toBe(2);
49 | expect(data).toContain({itemID: 1, display: 'Test1', toFilter: 'fee'});
50 | expect(data[2]).not.toBeDefined();
51 | expect(selection).toContain('1');
52 | expect(selection).toContain('2');
53 | }).always(done);
54 | });
55 |
56 | it('selection multiselect display-function', (done) => {
57 | let def = Deferred(), component = null, props = getPropsBigData();
58 |
59 | props.multiSelect = true;
60 | props.afterSelect = (data, selection) => {
61 | def.resolve(data, selection);
62 | }
63 | component = prepare(props);
64 |
65 | // Check selection
66 | component.triggerSelection(new Set(['1','item_2','item_5','item_6', 'item_8']));
67 |
68 | def.done((data, selection) => {
69 | expect(data.length).toBe(4);
70 | expect(selection.length).toBe(5);
71 | expect(selection).toContain('1');
72 | expect(selection).toContain('item_2');
73 | expect(selection).toContain('item_6');
74 | expect(selection).not.toContain('item_4');
75 | expect(data).toContain({itemID: 'item_8', display: formater, name: 'Tést 8',moreFields: 'moreFields values'})
76 | }).always(done);
77 | });
78 |
79 | it('selection singleselection', (done) => {
80 | let def = Deferred(), def2 = Deferred(), component = null, props = getPropsBigData();
81 |
82 | props.defaultSelection = null;
83 | props.afterSelect = (data, selection) => {
84 | if (data.length >= 1) {
85 | def.resolve(data, selection);
86 | } else {
87 | def2.resolve(data, selection)
88 | }
89 | }
90 | component = prepare(props);
91 | component.triggerSelection(new Set(['item_190']));
92 |
93 | def.done((data, selection) => {
94 | expect(data.length).toBe(1);
95 | expect(selection.length).toBe(1);
96 | expect(selection[0]).toBe('item_190');
97 | });
98 |
99 | component.triggerSelection(new Set([]));
100 | def2.done((data, selection) => {
101 | expect(data.length).toBe(0);
102 | expect(selection.length).toBe(0);
103 | }).always(done);
104 | });
105 |
106 | it('selectAll on filtered data', (done) => {
107 | let def = Deferred(), component = null, node = null, props = null;
108 | props = getPropsBigData();
109 |
110 | props.multiSelect = true;
111 | props.defaultSelection = null;
112 | props.afterSelect = (data, selection) => {
113 | if (data.length > 1) {
114 | def.resolve(data, selection);
115 | }
116 | }
117 |
118 | component = prepare(props);
119 | node = TestUtils.findRenderedDOMComponentWithClass(component, "list-bar-check");
120 |
121 | component.handleSearch('test 10'); // Filter
122 | TestUtils.Simulate.click(node); // Select All
123 | component.handleSearch(null); // Back to default
124 |
125 | def.done((data, selection) => {
126 | expect(selection.length).toBe(12);
127 | expect(data.length).toBe(12);
128 | }).always(done);
129 | });
130 |
131 | it('select/unselect all on filtered data && multiple operations', (done) => {
132 | let def = Deferred(), component = null, nodeCheckAll = null, nodeUnCheck = null, nodeElements = null, props = null, promise = null;
133 | props = getPropsBigData();
134 | promise = { done: () => {return;} }
135 |
136 | spyOn(promise, 'done');
137 |
138 | props.multiSelect = true;
139 | props.defaultSelection = null;
140 | props.afterSelect = (data, selection) => {
141 | if (promise.done.calls.any()) {
142 | def.resolve(data, selection);
143 | }
144 | }
145 |
146 | component = prepare(props);
147 | nodeCheckAll = TestUtils.findRenderedDOMComponentWithClass(component, "list-bar-check");
148 | nodeUnCheck = TestUtils.findRenderedDOMComponentWithClass(component, "list-bar-unCheck");
149 | nodeElements = TestUtils.scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
150 |
151 | TestUtils.Simulate.click(nodeCheckAll); // Select All 1000
152 | component.handleSearch('test 10'); // Filter (12 elements 1000 110 109... 100 10)
153 | TestUtils.Simulate.click(nodeUnCheck); // UnSelect All (1000 - 12 => 988)
154 | component.handleSearch(null); // Back to default
155 | TestUtils.Simulate.click(nodeElements[3]); // Unselect element 997 (988 - 1 => 987)
156 | component.handleSearch('test 11'); // Filter (11 elements 11 110 111... 116 117 118 119)
157 | TestUtils.Simulate.click(nodeUnCheck); // UnSelect All (987 - 11 => 976)
158 | TestUtils.Simulate.click(nodeCheckAll); // Select All (976 + 11 => 987)
159 | promise.done();
160 | nodeElements = TestUtils.scryRenderedDOMComponentsWithClass(component, "proper-search-list-element"); // update nodeElement to current in view
161 | TestUtils.Simulate.click(nodeElements[3]); // Click element 116 (unSelect) (987 - 1 => 986)
162 |
163 | def.done((data, selection) => {
164 | let set = new Set(selection);
165 |
166 | expect(selection.length).toBe(986);
167 | expect(data.length).toBe(986);
168 | expect(set.has('item_997')).toBe(false);
169 | expect(set.has('item_996')).toBe(true);
170 | expect(set.has('item_116')).toBe(false);
171 | expect(set.has('item_100')).toBe(false);
172 | expect(promise.done).toHaveBeenCalled();
173 |
174 | }).always(done);
175 | });
176 |
177 | it('keeps selection after refreshing data && update props', (done) => {
178 | let def = Deferred(), props = getPropsBigData(), promise = null, component = null, nodeCheckAll = null, nodeElements = null, newData = [];
179 | promise = { done: () => {return;} }
180 |
181 | spyOn(promise, 'done');
182 |
183 | props.multiSelect = true;
184 | props.defaultSelection = null;
185 | props.afterSelect = (data, selection) => {
186 | if (promise.done.calls.any()) {
187 | def.resolve(data, selection);
188 | }
189 | }
190 |
191 | component = ReactDOM.render( , wrapper);
192 |
193 | nodeCheckAll = TestUtils.findRenderedDOMComponentWithClass(component, "list-bar-check");
194 | TestUtils.Simulate.click(nodeCheckAll); // Select All 1000
195 |
196 | for (let i = 1000; i > 0; i--) {
197 | newData.push({id: 'item_' + i, display2: 'Element_' + i, name2: 'Testing2 ' + i});
198 | };
199 | props.data = newData;
200 | props.idField = 'id';
201 | props.displayField = 'display2';
202 | props.filterField = 'name2';
203 |
204 | component = ReactDOM.render( , wrapper); // Update props
205 | nodeElements = TestUtils.scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
206 |
207 | promise.done();
208 | TestUtils.Simulate.click(nodeElements[0]); // Unselect one element (Element 1000) to call afterSelect
209 |
210 | def.done((data, selection) => {
211 | expect(selection.length).toBe(999);
212 | expect(data[0]).toEqual(newData[1])
213 | expect(data[0].id).toBeDefined();
214 | expect(data[0].display2).toBeDefined();
215 | expect(data[0].name2).toBeDefined();
216 | expect(data[0].moreFields).not.toBeDefined();
217 | }).always(done);
218 | });
219 |
220 | });
221 |
222 | function prepare(props) {
223 | return TestUtils.renderIntoDocument( );
224 | }
225 |
226 | function getProps() {
227 | return {
228 | data: [{itemID:1, display: 'Test1', toFilter: 'fee'}, {itemID:2, display: 'Test2', toFilter: 'fuu'}, {itemID:3, display: 'Test3', toFilter: 'foo'}],
229 | lang: 'SPA',
230 | defaultSelection: [1],
231 | multiSelect: true,
232 | idField: 'itemID',
233 | displayField: 'display',
234 | filterField: 'toFilter',
235 | listWidth: 100,
236 | listHeight: 100
237 | }
238 | }
239 |
240 |
241 | function formater(listElement) {
242 | return { listElement.name } ;
243 | }
244 |
245 | function getPropsBigData(length = 1000) {
246 | let data = [];
247 |
248 | for (let i = length; i > 0; i--) {
249 | data.push({itemID: 'item_' + i, display: formater, name: 'Tést ' + i, moreFields: 'moreFields values'});
250 | };
251 |
252 | return {
253 | data: data,
254 | idField: 'itemID',
255 | defaultSelection: ['item_1'],
256 | displayField: 'display',
257 | filterField: 'name',
258 | listWidth: 100,
259 | listHeight: 100
260 | }
261 | }
--------------------------------------------------------------------------------
/src/jsx/components/__tests__/searchList-test.js:
--------------------------------------------------------------------------------
1 | import SearchList from '../searchList';
2 | import Immutable from 'immutable';
3 | import TestUtils from "react-addons-test-utils";
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 | import _ from 'underscore';
7 | import {Deferred} from 'jquery';
8 | import messages from "../../lang/messages";
9 | const Set = require('es6-set');
10 |
11 | describe('SearchList', function() {
12 |
13 | it('is available', () => {
14 | expect(SearchList).not.toBe(null);
15 | });
16 |
17 | it('handleElementClick singleSelection', (done) => {
18 | let def = Deferred();
19 | let props = getPropsBigData();
20 | let expected = 'item_89';
21 | props.multiSelect = false;
22 | props.onSelectionChange = (selection) => {
23 | def.resolve(selection);
24 | }
25 |
26 | let component = prepare(props);
27 |
28 | // Click elements
29 | let nodeElements = TestUtils.scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
30 | TestUtils.Simulate.click(nodeElements[11]);
31 |
32 | def.done((selection) => {
33 | expect(selection.has(expected)).toBe(true);
34 | expect(selection.size).toBe(1);
35 | }).always(done);
36 | });
37 |
38 | it('handleElementClick multiselect', (done) => {
39 | let def = Deferred();
40 | let props = getPropsBigData();
41 | let expected = new Set(['item_98', 'item_100', 'item_88', 'item_91']);
42 |
43 | props.onSelectionChange = (selection) => {
44 | if (selection.size >= 1) {
45 | def.resolve(selection);
46 | }
47 | }
48 |
49 | let component = prepare(props);
50 |
51 | // Click elements
52 | let nodeElements = TestUtils.scryRenderedDOMComponentsWithClass(component, "proper-search-list-element");
53 | TestUtils.Simulate.click(nodeElements[2]);
54 | TestUtils.Simulate.click(nodeElements[1]); // Old selected item (unselect now)
55 | TestUtils.Simulate.click(nodeElements[0]);
56 | TestUtils.Simulate.click(nodeElements[12]);
57 | TestUtils.Simulate.click(nodeElements[9]);
58 |
59 | def.done((selection) => {
60 | let result = true;
61 | selection.forEach( element => {
62 | if (!expected.has(element)) {
63 | result = false;
64 | return;
65 | }
66 | });
67 | expect(result).toBe(true);
68 | expect(selection.size).toBe(4);
69 | }).always(done);
70 | });
71 |
72 | it('handleSelectAll all', (done) => {
73 | let def = Deferred();
74 | let props = getPropsBigData();
75 |
76 | props.onSelectionChange = (selection) => {
77 | def.resolve(selection);
78 | }
79 |
80 | let component = prepare(props);
81 |
82 | // Click elements
83 | let node = TestUtils.findRenderedDOMComponentWithClass(component, "list-bar-check");
84 | TestUtils.Simulate.click(node);
85 |
86 | def.done((selection) => {
87 | expect(selection.size).toBe(100);
88 | }).always(done);
89 | });
90 |
91 | it('handleSelectAll nothing', (done) => {
92 | let def = Deferred();
93 | let props = getPropsBigData();
94 |
95 | props.onSelectionChange = (selection) => {
96 | def.resolve(selection);
97 | }
98 |
99 | let component = prepare(props);
100 |
101 | // Click elements
102 | let node = TestUtils.findRenderedDOMComponentWithClass(component, "list-bar-unCheck");
103 | TestUtils.Simulate.click(node);
104 |
105 | def.done((selection) => {
106 | expect(selection.size).toBe(0);
107 | }).always(done);
108 | });
109 | });
110 |
111 | function prepare(props) {
112 | return TestUtils.renderIntoDocument( );
113 | }
114 |
115 | function formater(listElement) {
116 | return { listElement.name } ;
117 | }
118 |
119 | function getPropsBigData(length = 100) {
120 | let data = [], preparedData = null;
121 |
122 | for (let i = length; i > 0; i--) {
123 | data.push({itemID: 'item_' + i, display: formater, name: 'Tést ' + i,moreFields: 'moreFields values'});
124 | };
125 |
126 | preparedData = prepareData(data, 'itemID');
127 |
128 | return {
129 | data: preparedData.data,
130 | indexedData: preparedData.indexed,
131 | idField: 'itemID',
132 | multiSelect: true,
133 | selection: new Set(['item_99']),
134 | displayField: 'display',
135 | uniqueID: 'test',
136 | messages: messages['ENG']
137 | }
138 | }
139 |
140 | /*
141 | * Prepare the data received by the component for the internal working.
142 | */
143 | function prepareData(newData, field) {
144 | // The data will be inmutable inside the component
145 | let data = Immutable.fromJS(newData), index = 0;
146 | let indexed = [], parsed = [];
147 |
148 | // Parsing data to add new fields (selected or not, field, rowIndex)
149 | parsed = data.map(row => {
150 | if (!row.get(field, false)) {
151 | row = row.set(field, _.uniqueId());
152 | } else {
153 | row = row.set(field, row.get(field).toString());
154 | }
155 |
156 | if (!row.get('_selected', false)) {
157 | row = row.set('_selected', false);
158 | }
159 |
160 | row = row.set('_rowIndex', index++);
161 |
162 | return row;
163 | });
164 |
165 | // Prepare indexed data.
166 | indexed = _.indexBy(parsed.toJSON(), field);
167 |
168 | return {
169 | rawdata: data,
170 | data: parsed,
171 | indexed: indexed
172 | };
173 | }
--------------------------------------------------------------------------------
/src/jsx/components/search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Immutable from 'immutable';
3 | import _ from 'underscore';
4 | import SearchList from './searchList';
5 | import SeachField from 'react-propersearch-field';
6 | import messages from "../lang/messages";
7 | import Normalizer from "../utils/normalize";
8 | import {shallowEqualImmutable} from 'react-immutable-render-mixin';
9 | import cache from '../lib/cache';
10 | const Set = require('es6-set');
11 |
12 | // For more info about this read ReadMe.md
13 | function getDefaultProps() {
14 | return {
15 | data: [],
16 | rawdata: null, // Case you want to use your own inmutable data. Read prepareData() method for more info.
17 | indexed: null, // Case you want to use your own inmutable data. Read prepareData() method for more info.
18 | messages: messages,
19 | lang: 'ENG',
20 | rowFormater: null, // function to format values in render
21 | defaultSelection: null,
22 | hiddenSelection: null,
23 | multiSelect: false,
24 | listWidth: null,
25 | listHeight: 200,
26 | listRowHeight: 26,
27 | afterSelect: null, // Function Get selection and data
28 | afterSelectGetSelection: null, // Function Get just selection (no data)
29 | afterSearch: null,
30 | onEnter: null, // Optional - To do when key down Enter - SearchField
31 | fieldClass: null,
32 | listClass: null,
33 | listElementClass: null,
34 | className: null,
35 | placeholder: 'Search...',
36 | searchIcon: 'fa fa-search fa-fw',
37 | clearIcon: 'fa fa-times fa-fw',
38 | throttle: 160, // milliseconds
39 | minLength: 3,
40 | defaultSearch: null,
41 | autoComplete: 'off',
42 | idField: 'value',
43 | displayField: 'label',
44 | listShowIcon: true,
45 | filter: null, // Optional function (to be used when the displayField is an function too)
46 | filterField: null, // By default it will be the displayField
47 | allowsEmptySelection: false, // Put this to true to get a diferent ToolBar that allows select empty
48 | }
49 | }
50 |
51 |
52 | /**
53 | * A proper search component for react. With a search field and a list of items allows the user to filter that list and select the items.
54 | * The component return the selected data when it's selected. Allows multi and single selection. The list is virtual rendered, was designed
55 | * to handle thousands of elements without sacrificing performance, just render the elements in the view. Used react-virtualized to render the list items.
56 | *
57 | * Simple example usage:
58 | *
59 | * let data = [];
60 | * data.push({
61 | * value: 1,
62 | * label: 'Apple'
63 | * });
64 | *
65 | * let afterSelect = (data, selection) => {
66 | * console.info(data);
67 | * console.info(selection);
68 | * }
69 | *
70 | *
75 | * ```
76 | */
77 | class Search extends React.Component {
78 | constructor(props) {
79 | super(props);
80 |
81 | let preparedData = this.prepareData(null, props.idField, false, props.displayField);
82 |
83 | this.state = {
84 | data: preparedData.data, // Data to work with (Inmutable)
85 | initialData: preparedData.data, // Same data as initial state.data but this data never changes. (Inmutable)
86 | rawData: preparedData.rawdata, // Received data without any modfication (Inmutable)
87 | indexedData: preparedData.indexed, // Received data indexed (No Inmutable)
88 | initialIndexed: preparedData.indexed, // When data get filtered keep the full indexed
89 | idField: props.idField, // To don't update the idField if that field doesn't exist in the fields of data array
90 | displayField: props.displayField, // same
91 | selection: new Set(),
92 | allSelected: false,
93 | selectionApplied: false, // If the selection has been aplied to the data (mostly for some cases of updating props data)
94 | ready: false
95 | }
96 | }
97 |
98 | componentDidMount() {
99 | this.setDefaultSelection(this.props.defaultSelection);
100 |
101 | this.setState({
102 | ready: true
103 | });
104 | }
105 |
106 | shouldComponentUpdate(nextProps, nextState){
107 | let stateChanged = !shallowEqualImmutable(this.state, nextState);
108 | let propsChanged = !shallowEqualImmutable(this.props, nextProps);
109 | let somethingChanged = propsChanged || stateChanged;
110 |
111 | // Update row indexes when data get filtered
112 | if (this.state.data.size != nextState.data.size) {
113 | let parsed, indexed, data;
114 |
115 | if (nextState.ready) {
116 | if (nextState.data.size === 0) {
117 | data = nextState.data;
118 | indexed = {};
119 | } else {
120 | parsed = this.prepareData(nextState.data, this.state.idField, true, this.state.displayField); // Force rebuild indexes etc
121 | data = parsed.data;
122 | indexed = parsed.indexed;
123 | }
124 |
125 | this.setState({
126 | data: data,
127 | indexedData: indexed,
128 | allSelected: this.isAllSelected(data, nextState.selection)
129 | });
130 | } else {
131 | let selection = nextProps.defaultSelection;
132 | if (!nextProps.multiSelect) selection = nextState.selection.values().next().value || null;
133 |
134 | // props data has been changed in the last call to this method
135 | this.setDefaultSelection(selection);
136 | if (_.isNull(selection) || selection.length === 0) this.setState({ready: true}); // No def selection so then ready
137 | }
138 |
139 | return false;
140 | }
141 |
142 | if (propsChanged) {
143 | let dataChanged = !shallowEqualImmutable(this.props.data, nextProps.data);
144 | let idFieldChanged = this.props.idField != nextProps.idField, displayFieldChanged = this.props.displayField != nextProps.displayField;
145 | let selectionChanged = false, nextSelection = new Set(nextProps.defaultSelection), selection = null;
146 |
147 | if (this.state.selection.size != nextSelection.size) {
148 | selectionChanged = true;
149 | selection = nextProps.defaultSelection;
150 | } else {
151 | this.state.selection.forEach(element => {
152 | if (!nextSelection.has(element)) {
153 | selectionChanged = true;
154 | selection = nextProps.defaultSelection;
155 | return true;
156 | }
157 | });
158 | }
159 |
160 | if (!nextProps.multiSelect && (nextSelection.size > 1 || this.state.selection.size > 1)) {
161 | selection = nextSelection.size > 1 ? nextProps.defaultSelection : this.state.selection.values().next().value;
162 | if (_.isArray(selection)) selection = selection[0];
163 | }
164 |
165 | if (idFieldChanged || displayFieldChanged) {
166 | let fieldsSet = new Set(_.keys(nextProps.data[0]));
167 | let messages = this.getTranslatedMessages();
168 |
169 | // Change idField / displayField but that field doesn't exist in the data
170 | if (!fieldsSet.has(nextProps.idField) || !fieldsSet.has(nextProps.displayField)) {
171 | if (!fieldsSet.has(nextProps.idField)) console.error(messages.errorIdField + ' ' + nextProps.idField + ' ' + messages.errorData);
172 | else console.error(messages.errorDisplayField + ' ' + nextProps.idField + ' ' + messages.errorData);
173 |
174 | return false;
175 | } else { // New idField &&//|| displayField exist in data array fields
176 | if (dataChanged){
177 | cache.flush('search_list');
178 | let preparedData = this.prepareData(nextProps.data, nextProps.idField, false, nextProps.displayField);
179 |
180 | this.setState({
181 | data: preparedData.data,
182 | initialData: preparedData.data,
183 | rawData: preparedData.rawdata,
184 | indexedData: preparedData.indexed,
185 | initialIndexed: preparedData.indexed,
186 | idField: nextProps.idField,
187 | displayField: nextProps.displayField,
188 | ready: false,
189 | selectionApplied: false
190 | }, this.setDefaultSelection(selection));
191 |
192 | } else {
193 | let initialIndexed = null, indexed = null;
194 |
195 | // If the id field change then the indexed data has to be changed but not for display
196 | if (displayFieldChanged) {
197 | initialIndexed = this.state.initialIndexed;
198 | indexed = this.state.indexedData;
199 | } else {
200 | initialIndexed = _.indexBy(this.state.initialData.toJSON(), nextProps.idField);
201 | indexed = _.indexBy(this.state.data.toJSON(), nextProps.idField);
202 | }
203 |
204 | this.setState({
205 | indexedData: indexed,
206 | initialIndexed: initialIndexed,
207 | idField: nextProps.idField,
208 | displayField: nextProps.displayField,
209 | ready: false,
210 | selectionApplied: false
211 | });
212 | }
213 | return false;
214 | }
215 | }
216 |
217 | if (dataChanged){
218 | cache.flush('search_list');
219 | let preparedData = this.prepareData(nextProps.data, nextProps.idField, false, nextProps.displayField);
220 |
221 | this.setState({
222 | data: preparedData.data,
223 | initialData: preparedData.data,
224 | rawData: preparedData.rawdata,
225 | indexedData: preparedData.indexed,
226 | initialIndexed: preparedData.indexed,
227 | ready: false,
228 | selectionApplied: false
229 | }, this.setDefaultSelection(selection));
230 |
231 | return false;
232 | }
233 |
234 | if (selectionChanged) {
235 | // Default selection does nothing if the selection is null so in that case update the state to restart selection
236 | if (!_.isNull(selection)) {
237 | this.setDefaultSelection(selection);
238 | } else {
239 | this.setState({
240 | selection: new Set(),
241 | allSelected: false,
242 | ready: true
243 | });
244 | }
245 |
246 | return false;
247 | }
248 | }
249 |
250 | if (!nextState.ready) {
251 | this.setState({
252 | ready: true
253 | });
254 |
255 | return false;
256 | }
257 |
258 | return somethingChanged;
259 | }
260 |
261 | /**
262 | * Before the components update set the updated selection data to the components state.
263 | *
264 | * @param {object} nextProps The props that will be set for the updated component
265 | * @param {object} nextState The state that will be set for the updated component
266 | */
267 | componentWillUpdate(nextProps, nextState) {
268 | // Selection
269 | if (this.props.multiSelect) {
270 | if (nextState.selection.size !== this.state.selection.size || (!nextState.selectionApplied && nextState.selection.size > 0)){
271 | this.updateSelectionData(nextState.selection, nextState.allSelected);
272 | }
273 | } else {
274 | let next = nextState.selection.values().next().value || null;
275 | let old = this.state.selection.values().next().value || null;
276 | let oldSize = !_.isNull(this.state.selection) ? this.state.selection.size : 0;
277 |
278 | if (next !== old || oldSize > 1){
279 | this.updateSelectionData(next);
280 | }
281 | }
282 |
283 | }
284 |
285 | /**
286 | * Method called before the components update to set the new selection to states component and update the data
287 | *
288 | * @param {array} newSelection The new selected rows (Set object)
289 | * @param {array} newAllSelected If the new state has all the rows selected
290 | */
291 | updateSelectionData(newSelection, newAllSelected = false) {
292 | let newIndexed = _.clone(this.state.indexedData);
293 | let oldSelection = this.state.selection;
294 | let rowid = null, selected = null, rdata = null, curIndex = null, newData = this.state.data, rowIndex = null;
295 | let newSelectionSize = !_.isNull(newSelection) ? newSelection.size : 0;
296 |
297 | // If oldSelection size is bigger than 1 that mean's the props has changed from multiselect to single select so if there is some list items with the selected class
298 | // if should be reset.
299 | if (!this.props.multiSelect && oldSelection.size <= 1) { // Single select
300 | let oldId = oldSelection.values().next().value || null;
301 | let indexedKeys = new Set(_.keys(newIndexed));
302 |
303 | if (!_.isNull(oldId) && indexedKeys.has(oldId)) {
304 | newIndexed[oldId]._selected = false; // Update indexed data
305 | rowIndex = newIndexed[oldId]._rowIndex; // Get data index
306 | if (newData.get(rowIndex)) {
307 | rdata = newData.get(rowIndex).set('_selected', false); // Change the row in that index
308 | newData = newData.set(rowIndex, rdata); // Set that row in the data object
309 | }
310 | }
311 |
312 | if (!_.isNull(newSelection) && indexedKeys.has(newSelection)) {
313 | newIndexed[newSelection]._selected = true; // Update indexed data
314 | rowIndex = newIndexed[newSelection]._rowIndex; // Get data index
315 | rdata = newData.get(rowIndex).set('_selected', true); // Change the row in that index
316 | newData = newData.set(rowIndex, rdata); // Set that row in the data object
317 | }
318 |
319 | } else if (!newAllSelected && this.isSingleChange(newSelectionSize)) { // Change one row data at a time
320 | let changedId = null, selected = null;
321 |
322 | // If the new selection has not one of the ids of the old selection that means an selected element has been unselected.
323 | oldSelection.forEach(id => {
324 | if (!newSelection.has(id)) {
325 | changedId = id;
326 | selected = false;
327 | return false;
328 | }
329 | });
330 |
331 | // Otherwise a new row has been selected. Look through the new selection for the new element.
332 | if (!changedId) {
333 | selected = true;
334 | newSelection.forEach(id => {
335 | if (!oldSelection.has(id)) {
336 | changedId = id;
337 | return false;
338 | }
339 | });
340 | }
341 |
342 | newIndexed[changedId]._selected = selected; // Update indexed data
343 | rowIndex = newIndexed[changedId]._rowIndex; // Get data index
344 | rdata = newData.get(rowIndex).set('_selected', selected); // Change the row in that index
345 | newData = newData.set(rowIndex, rdata); // Set that row in the data object
346 | } else { // Change all data
347 | if (_.isNull(newSelection)) newSelection = new Set();
348 | else if (!_.isObject(newSelection)) newSelection = new Set([newSelection]);
349 |
350 | newData = newData.map((row) => {
351 | rowid = row.get(this.state.idField);
352 | selected = newSelection.has(rowid.toString());
353 | rdata = row.set('_selected', selected);
354 | curIndex = newIndexed[rowid];
355 |
356 | if (curIndex._selected != selected) { // update indexed data
357 | curIndex._selected = selected;
358 | newIndexed[rowid] = curIndex;
359 | }
360 |
361 | return rdata;
362 | });
363 | }
364 |
365 | this.setState({
366 | data: newData,
367 | indexedData: newIndexed,
368 | selectionApplied: true
369 | });
370 | }
371 |
372 | /**
373 | * Check if the selection has more than 1 change.
374 | *
375 | * @param {integer} newSize Size of the new selection
376 | */
377 | isSingleChange(newSize) {
378 | let oldSize = this.state.selection.size;
379 |
380 | if (oldSize - 1 == newSize || oldSize + 1 == newSize) return true;
381 | else return false;
382 | }
383 |
384 | /**
385 | * Get the translated messages for the component.
386 | *
387 | * @return object Messages of the selected language or in English if the translation for this lang doesn't exist.
388 | */
389 | getTranslatedMessages() {
390 | if (!_.isObject(this.props.messages)) {
391 | return {};
392 | }
393 |
394 | if (this.props.messages[this.props.lang]) {
395 | return this.props.messages[this.props.lang];
396 | }
397 |
398 | return this.props.messages['ENG'];
399 | }
400 |
401 | /**
402 | * In case that the new selection array be different than the selection array in the components state, then update
403 | * the components state with the new data.
404 | *
405 | * @param {array} newSelection The selected rows
406 | * @param {boolean} sendSelection If the selection must be sent or not
407 | */
408 | triggerSelection(newSelection = new Set(), sendSelection = true) {
409 | if (sendSelection) {
410 | this.setState({
411 | selection: newSelection,
412 | allSelected: this.isAllSelected(this.state.data, newSelection)
413 | }, this.sendSelection);
414 | } else {
415 | this.setState({
416 | selection: newSelection,
417 | allSelected: this.isAllSelected(this.state.data, newSelection)
418 | });
419 | }
420 | }
421 |
422 | /**
423 | * Check if all the current data are selected.
424 | *
425 | * @param {array} data The data to compare with selection
426 | * @param {object} selection The current selection Set of values (idField)
427 | */
428 | isAllSelected(data, selection) {
429 | let result = true;
430 | if (data.size > selection.size) return false;
431 |
432 | data.forEach((item, index) => {
433 | if (!selection.has(item.get(this.state.idField, null))) { // Some data not in selection
434 | result = false;
435 | return false;
436 | }
437 | });
438 |
439 | return result;
440 | }
441 |
442 | /**
443 | * Set up the default selection if exist
444 | *
445 | * @param {array || string ... number} defSelection Default selection to be applied to the list
446 | */
447 | setDefaultSelection(defSelection) {
448 | if (defSelection) {
449 | let selection = null;
450 |
451 | if (defSelection.length == 0) {
452 | selection = new Set();
453 | } else {
454 | if (!_.isArray(defSelection)) {
455 | selection = new Set([defSelection.toString()]);
456 | } else {
457 | selection = new Set(defSelection.toString().split(','));
458 | }
459 | }
460 |
461 | selection.delete(''); // Remove empty values
462 |
463 | this.triggerSelection(selection, false);
464 | }
465 | }
466 |
467 | /**
468 | * Prepare the data received by the component for the internal use.
469 | *
470 | * @param (object) newData New data for rebuild. (filtering || props changed)
471 | * @param (string) idField New idField if it has been changed. (props changed)
472 | * @param (boolean) rebuild Rebuild the data. NOTE: If newData its an Immutable you should put this param to true.
473 | *
474 | * @return (array) -rawdata: The same data as the props or the newData in case has been received.
475 | * -indexed: Same as rawdata but indexed by the idField
476 | * -data: Parsed data to add some fields necesary to internal working.
477 | */
478 | prepareData(newData = null, idField = null, rebuild = false, displayfield = null) {
479 | // The data will be inmutable inside the component
480 | let data = newData || this.props.data, index = 0, rdataIndex = 0, idSet = new Set(), field = idField || this.state.idField, fieldValue;
481 | let indexed = [], parsed = [], rawdata, hasNulls = false;
482 |
483 | // If not Immutable.
484 | // If an Immutable is received in props.data at the components first building the component will work with that data. In that case
485 | // the component should get indexed and rawdata in props. It's up to the developer if he / she wants to work with data from outside
486 | // but it's important to keep in mind that you need a similar data structure (_selected, _rowIndex, idField...)
487 | if (!Immutable.Iterable.isIterable(data) || rebuild) {
488 | data = Immutable.fromJS(data); // If data it's already Immutable the method .fromJS return the same object
489 |
490 | // Parsing data to add new fields (selected or not, field, rowIndex)
491 | parsed = data.map(row => {
492 | fieldValue = row.get(field, false);
493 |
494 | if (!fieldValue) {
495 | fieldValue = _.uniqueId();
496 | }
497 |
498 | // No rows with same idField. The idField must be unique and also don't render the empty values
499 | if (!idSet.has(fieldValue) && fieldValue !== '' && row.get(displayfield, '') !== '') {
500 | idSet.add(fieldValue);
501 | row = row.set(field, fieldValue.toString());
502 |
503 | if (!row.get('_selected', false)) {
504 | row = row.set('_selected', false);
505 | }
506 |
507 | row = row.set('_rowIndex', index++); // data row index
508 | row = row.set('_rawDataIndex', rdataIndex++); // rawData row index
509 |
510 | return row;
511 | }
512 |
513 | rdataIndex++; // add 1 to jump over duplicate values
514 | hasNulls = true;
515 | return null;
516 | });
517 |
518 | // Clear null values if exist
519 | if (hasNulls) {
520 | parsed = parsed.filter(element => !_.isNull(element));
521 | }
522 |
523 | // Prepare indexed data.
524 | indexed = _.indexBy(parsed.toJSON(), field);
525 |
526 | } else { // In case received Inmutable data, indexed data and raw data in props.
527 | data = this.props.rawdata;
528 | parsed = this.props.data;
529 | indexed = this.props.indexed;
530 | }
531 |
532 |
533 | return {
534 | rawdata: data,
535 | data: parsed,
536 | indexed: indexed
537 | };
538 | }
539 |
540 | /**
541 | * Function called each time the selection has changed. Apply an update in the components state selection then render again an update the child
542 | * list.
543 | *
544 | * @param (Set object) selection The selected values using the values of the selected data.
545 | * @param (Boolean) emptySelection When allowsEmptySelection is true and someone wants the empty selection.
546 | */
547 | handleSelectionChange(selection, emptySelection = false) {
548 | if (!emptySelection) {
549 | this.triggerSelection(selection);
550 | } else {
551 | this.sendEmptySelection();
552 | }
553 | }
554 |
555 | /**
556 | * Function called each time the search field has changed. Filter the data by using the received search field value.
557 | *
558 | * @param (String) value String written in the search field
559 | */
560 | handleSearch(value) {
561 | let lValue = value ? value : null, filter = null;
562 | let data = this.state.initialData, filteredData = data, selection = this.state.selection;
563 | let displayField = this.state.displayField, idField = this.state.idField;
564 | let hasFilter = (typeof this.props.filter == 'function');
565 |
566 | // When the search field has been clear then the value will be null and the data will be the same as initialData, otherwise
567 | // the data will be filtered using the .filter() function of Inmutable lib. It return a Inmutable obj with the elements that
568 | // match the expresion in the parameter.
569 | if (value) {
570 | lValue = Normalizer.normalize(lValue);
571 |
572 | // If the prop `filter´ has a function then use if to filter as an interator over the indexed data.
573 | if (hasFilter) {
574 | let filtered = null, filteredIndexes = new Set();
575 |
576 | // Filter indexed data using the funtion
577 | _.each(this.state.initialIndexed, element => {
578 | if (this.props.filter(element, lValue)) {
579 | filteredIndexes.add(element._rowIndex);
580 | }
581 | });
582 |
583 | // Then get the data that match with that indexed data
584 | filteredData = data.filter(element => {
585 | return filteredIndexes.has(element.get('_rowIndex'));
586 | });
587 | } else {
588 | filteredData = data.filter(element => {
589 | filter = element.get(this.props.filterField, null) || element.get(displayField);
590 |
591 | // When it's a function then use the field in filterField to search, if this field doesn't exist then use the field name or then idField.
592 | if (typeof filter == 'function') {
593 | filter = element.get('name', null) || element.get(idField);
594 | }
595 |
596 | filter = Normalizer.normalize(filter);
597 | return filter.indexOf(lValue) >= 0;
598 | });
599 | }
600 | }
601 |
602 | // Apply selection
603 | filteredData = filteredData.map(element => {
604 | if (selection.has(element.get(idField, null))) {
605 | element = element.set('_selected', true);
606 | }
607 |
608 | return element;
609 | });
610 |
611 | this.setState({
612 | data: filteredData
613 | }, this.sendSearch(lValue));
614 | }
615 |
616 | /**
617 | * Get the data that match with the selection in params and send the data and the selection to a function whichs name is afterSelect
618 | * if this function was set up in the component props
619 | */
620 | sendSelection() {
621 | let hasAfterSelect = typeof this.props.afterSelect == 'function', hasGetSelection = typeof this.props.afterSelectGetSelection == 'function';
622 |
623 | if (hasAfterSelect || hasGetSelection) {
624 | let selectionArray = [], selection = this.state.selection;
625 |
626 | // Parse the selection to return it as an array instead of a Set obj
627 | selection.forEach(item => {
628 | selectionArray.push(item.toString());
629 | });
630 |
631 | if (hasGetSelection) { // When you just need the selection but no data
632 | this.props.afterSelectGetSelection.call(this, selectionArray, selection); // selection array / selection Set()
633 | }
634 |
635 | if (hasAfterSelect) {
636 | let selectedData = [], properId = null, rowIndex = null, filteredData = null;
637 | let {indexedData, initialData, rawData, data} = this.state;
638 | let fields = new Set(_.keys(rawData.get(0).toJSON())), hasIdField = fields.has(this.state.idField) ? true : false;
639 |
640 | if (hasIdField) {
641 | selectedData = rawData.filter(element => {
642 | return selection.has(element.get(this.state.idField).toString());
643 | });
644 | } else {
645 | // Get the data (initialData) that match with the selection
646 | filteredData = initialData.filter(element => selection.has(element.get(this.state.idField)));
647 |
648 | // Then from the filtered data get the raw data that match with the selection
649 | selectedData = filteredData.map(row => {
650 | properId = row.get(this.state.idField);
651 | rowIndex = this.state.initialIndexed[properId]._rawDataIndex;
652 |
653 | return rawData.get(rowIndex);
654 | });
655 | }
656 |
657 | this.props.afterSelect.call(this, selectedData.toJSON(), selectionArray);
658 | }
659 | }
660 | }
661 |
662 | sendEmptySelection() {
663 | let hasAfterSelect = typeof this.props.afterSelect == 'function', hasGetSelection = typeof this.props.afterSelectGetSelection == 'function';
664 |
665 | if (hasAfterSelect || hasGetSelection) {
666 | if (hasGetSelection) { // When you just need the selection but no data
667 | this.props.afterSelectGetSelection.call(this, [''], new Set(''));
668 | }
669 |
670 | if (hasAfterSelect) {
671 | let filteredData = null, rawData = this.state.rawData, id, display;
672 |
673 | // Get the data (rawData) that have idField or displayfield equals to empty string
674 | filteredData = rawData.filter(element => {
675 | id = element.get(this.state.idField);
676 | display = element.get(this.state.displayField);
677 | return display === '' || display === null || id === '' || id === null;
678 | });
679 |
680 | this.props.afterSelect.call(this, filteredData.toJSON(), ['']);
681 | }
682 | }
683 | }
684 |
685 | /**
686 | * Send the written string in the search field to the afterSearch function if it was set up in the components props
687 | *
688 | * @param (String) searchValue String written in the search field
689 | */
690 | sendSearch(searchValue) {
691 | if (typeof this.props.afterSearch == 'function') {
692 | this.props.afterSearch.call(this, searchValue);
693 | }
694 | }
695 |
696 | render() {
697 | let messages = this.getTranslatedMessages(),
698 | content = null,
699 | data = this.state.data,
700 | selection = new Set(),
701 | allSelected = this.state.allSelected,
702 | className = "proper-search";
703 |
704 | if (this.props.className) {
705 | className += ' '+this.props.className;
706 | }
707 |
708 | if (this.state.ready) {
709 | this.state.selection.forEach( element => {
710 | selection.add(element);
711 | });
712 |
713 | content = (
714 |
715 |
727 |
748 |
749 | );
750 |
751 | } else {
752 | content = {messages.loading}
753 | }
754 |
755 | return (
756 |
757 | {content}
758 |
759 | );
760 | }
761 | };
762 |
763 | Search.defaultProps = getDefaultProps();
764 | export default Search;
--------------------------------------------------------------------------------
/src/jsx/components/searchList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'underscore';
3 | import {shallowEqualImmutable} from 'react-immutable-render-mixin';
4 | import { VirtualScroll } from 'react-virtualized';
5 | import Dimensions from 'react-dimensions';
6 | import cache from '../lib/cache';
7 | const Set = require('es6-set');
8 |
9 | // For more info about this read ReadMe.md
10 | function getDefaultProps() {
11 | return {
12 | data: null,
13 | indexedData: null, // Just when you use a function as a display field. (array) (Full indexed data not filted)
14 | onSelectionChange: null,
15 | rowFormater: null, // function
16 | multiSelect: false,
17 | messages: null,
18 | selection: new Set(),
19 | allSelected: false,
20 | listRowHeight: 26,
21 | listHeight: 200,
22 | listWidth: 100, // Container width by default
23 | idField: 'value',
24 | displayField: 'label',
25 | showIcon: true,
26 | listElementClass: null,
27 | allowsEmptySelection: false,
28 | hiddenSelection: null,
29 | uniqueID: null,
30 | }
31 | }
32 |
33 | /**
34 | * A Component that render a list of selectable items, with single or multiple selection and return the selected items each time a new item be selected.
35 | *
36 | * Simple example usage:
37 | * let handleSelection = function(selection){
38 | * console.log('Selected values: ' + selection) // The selection is a Set obj
39 | * }
40 | *
41 | *
47 | * ```
48 | */
49 | class SearchList extends React.Component {
50 | constructor(props){
51 | super(props);
52 |
53 | this.state = {
54 | allSelected: props.allSelected,
55 | nothingSelected: props.selection.size == 0,
56 | hiddenSelection: new Set()
57 | }
58 | }
59 |
60 | componentWillMount() {
61 | this.uniqueId = this.props.uniqueID ? this.props.uniqueID : _.uniqueId('search_list_');
62 | if (this.props.hiddenSelection) {
63 | this.setState({
64 | hiddenSelection: this.parseHiddenSelection(this.props)
65 | });
66 | }
67 | }
68 |
69 | shouldComponentUpdate(nextProps, nextState){
70 | let propschanged = !shallowEqualImmutable(this.props, nextProps);
71 | let stateChanged = !shallowEqualImmutable(this.state, nextState);
72 | let somethingChanged = propschanged || stateChanged;
73 |
74 | if (propschanged) {
75 | let nothingSelected = false;
76 |
77 | if (!nextProps.allSelected) nothingSelected = this.isNothingSelected(nextProps.data, nextProps.selection);
78 |
79 | // When the props change update the state.
80 | if (nextProps.allSelected != this.state.allSelected || nothingSelected != this.state.nothingSelected) {
81 | this.setState({
82 | allSelected: nextProps.allSelected,
83 | nothingSelected: nothingSelected
84 | });
85 | }
86 | }
87 |
88 | return somethingChanged;
89 | }
90 |
91 | componentWillReceiveProps(newProps) {
92 | let hiddenChange = !shallowEqualImmutable(this.props.hiddenSelection, newProps.hiddenSelection);
93 | let hiddenSelection;
94 |
95 | if (hiddenChange) {
96 | if (this._virtualScroll) this._virtualScroll.recomputeRowHeights(0);
97 | hiddenSelection = this.parseHiddenSelection(newProps);
98 | this.setState({
99 | hiddenSelection: hiddenSelection
100 | });
101 | }
102 | }
103 |
104 | /**
105 | * Function called each time an element of the list is selected. Get the value (value of the idField) of the
106 | * element that was selected, them change the selection and call to onSelectionChange function in the props sending
107 | * the new selection.
108 | *
109 | * @param (String) itemValue Value of the idField of the selected element
110 | * @param (Array) e Element which call the function
111 | */
112 | handleElementClick(itemValue, e) {
113 | e.preventDefault();
114 | let data = this.props.data, selection = this.props.selection, nothingSelected = false, allSelected = false;
115 |
116 | if (this.props.multiSelect) {
117 | if (selection.has(itemValue)) {
118 | selection.delete(itemValue);
119 | } else {
120 | selection.add(itemValue);
121 | }
122 | } else {
123 | if (selection.has(itemValue)) selection = new Set();
124 | else selection = new Set([itemValue]);
125 | }
126 |
127 | allSelected = this.isAllSelected(data, selection);
128 | if (!allSelected) nothingSelected = this.isNothingSelected(data, selection);
129 |
130 | // If the state has changed update it
131 | if (allSelected != this.state.allSelected || nothingSelected != this.state.nothingSelected){
132 | this.setState({
133 | allSelected: allSelected,
134 | nothingSelected: nothingSelected
135 | });
136 | }
137 |
138 | if (typeof this.props.onSelectionChange == 'function') {
139 | this.props.onSelectionChange.call(this, selection);
140 | }
141 | }
142 |
143 | /**
144 | * Check if all the current data are not selected
145 | *
146 | * @param (array) data The data to compare with selection
147 | * @param (object) selection The current selection Set of values (idField)
148 | */
149 | isNothingSelected(data, selection) {
150 | let result = true;
151 | if (selection.size == 0) return true;
152 |
153 | data.forEach(element => {
154 | if (selection.has(element.get(this.props.idField, null))) { // Some data not in selection
155 | result = false;
156 | return false;
157 | }
158 | });
159 |
160 | return result;
161 | }
162 |
163 | /**
164 | * Check if all the current data are selected.
165 | *
166 | * @param (array) data The data to compare with selection
167 | * @param (object) selection The current selection Set of values (idField)
168 | */
169 | isAllSelected(data, selection) {
170 | let result = true;
171 | if (data.size > selection.size) return false;
172 |
173 | data.forEach(element => {
174 | if (!selection.has(element.get(this.props.idField, null))) { // Some data not in selection
175 | result = false;
176 | return false;
177 | }
178 | });
179 |
180 | return result;
181 | }
182 |
183 | /**
184 | * Function called each time the buttons in the bar of the list has been clicked. Delete or add all the data elements into the selection, just if it has changed.
185 | *
186 | * @param (Boolean) selectAll If its a select all action or an unselect all.
187 | * @param (Array) e Element which call the function
188 | */
189 | handleSelectAll(selectAll, e) {
190 | e.preventDefault();
191 |
192 | let newData = [], data = this.props.data, field = this.props.idField;
193 | let selection = this.props.selection;
194 | let hasChanged = (selectAll != this.state.allSelected || (!selectAll && !this.state.nothingSelected)); // nothingSelected = false then something is selected
195 |
196 | if (selectAll && hasChanged) {
197 | data.forEach(element => {
198 | selection.add(element.get(field, null));
199 | });
200 | } else {
201 | data.forEach(element => {
202 | selection.delete(element.get(field, null));
203 | });
204 | }
205 |
206 | if (hasChanged) {
207 | this.setState({
208 | allSelected: selectAll,
209 | nothingSelected: !selectAll
210 | });
211 | }
212 |
213 | if (typeof this.props.onSelectionChange == 'function' && hasChanged) {
214 | this.props.onSelectionChange.call(this, selection);
215 | }
216 | }
217 |
218 | /**
219 | * Function called each time the buttons (select empty) in the bar of the list has been clicked (In case empty selection allowed).
220 | *
221 | * @param (Array) e Element which call the function
222 | */
223 | handleSelectEmpty( e) {
224 | if (typeof this.props.onSelectionChange == 'function') {
225 | this.props.onSelectionChange.call(this, null, true);
226 | }
227 | }
228 |
229 | /**
230 | * Parse the hidden selection if that property contains somethings.
231 | *
232 | * @param (array) props Component props (or nextProps)
233 | * @return (Set) hiddenSelection The hidden rows.
234 | */
235 | parseHiddenSelection(props = this.props) {
236 | let hidden = [], isArray = _.isArray(props.hiddenSelection), isObject = _.isObject(props.hiddenSelection);
237 |
238 | if (!isArray && isObject) return props.hiddenSelection; // Is Set
239 |
240 | if (!isArray) { // Is String or number
241 | hidden = [props.hiddenSelection.toString()];
242 | } else if (props.hiddenSelection.length > 0) { // Is Array
243 | hidden = props.hiddenSelection.toString().split(',');
244 | }
245 |
246 | return new Set(hidden);
247 | }
248 |
249 | /**
250 | * Return the tool bar for the top of the list. It will be displayed only when the selection can be multiple.
251 | *
252 | * @return (html) The toolbar code
253 | */
254 | getToolbar() {
255 | let maxWidth = this.props.containerWidth ? (this.props.containerWidth / 2) - 1 : 100;
256 |
257 | return (
258 |
277 | );
278 | }
279 |
280 | /**
281 | * Return the tool bar for the top of the list in case Empty Selection allowed
282 | *
283 | * @return (html) The toolbar code
284 | */
285 | getToolbarForEmpty() {
286 | let allSelected = this.state.allSelected, selectMessage, maxWidth = (this.props.containerWidth / 2) - 1;
287 | selectMessage = allSelected ? this.props.messages.none : this.props.messages.all;
288 |
289 | return (
290 |
310 | );
311 | }
312 |
313 | /**
314 | * Build and return the content of the list.
315 | *
316 | * @param (object) contentData
317 | * - index (integer) Index of the data to be rendered
318 | * - isScrolling (bool) If grid is scrollings
319 | * @return (html) list-row A row of the list
320 | */
321 | getContent(contentData) {
322 | let index = contentData.index;
323 | let icon = null, selectedClass = null, className = null, element = null, listElementClass = this.props.listElementClass;
324 | let data = this.props.data, rowdata, id, displayField = this.props.displayField, showIcon = this.props.showIcon;
325 |
326 | rowdata = data.get(index);
327 | element = rowdata.get(displayField);
328 | className = "proper-search-list-element";
329 | id = rowdata.get(this.props.idField);
330 |
331 | if (this.props.multiSelect) {
332 | if (showIcon) {
333 | if (rowdata.get('_selected', false)) {
334 | icon = ;
335 | selectedClass = ' proper-search-selected'
336 | } else {
337 | icon = ;
338 | selectedClass = null;
339 | }
340 | }
341 | } else {
342 | if (rowdata.get('_selected')) selectedClass = ' proper-search-single-selected';
343 | else selectedClass = null;
344 | }
345 |
346 | if (listElementClass) {
347 | className += ' ' + listElementClass;
348 | }
349 |
350 | if (selectedClass) {
351 | className += ' ' + selectedClass;
352 | }
353 |
354 | if (typeof element == 'function') {
355 | element = element(this.props.indexedData[id]);
356 | } else if (this.props.rowFormater) {
357 | let ckey = ['search_list', 'list_'+ this.uniqueId, 'row__'+rowdata.get(this.props.idField), displayField];
358 | element = cache.read(ckey);
359 |
360 | if (element === undefined) {
361 | element = this.props.rowFormater(rowdata.get(displayField));
362 | cache.write(ckey, element);
363 | }
364 | }
365 |
366 | return (
367 |
368 | {icon}
369 | {element}
370 |
371 | );
372 | }
373 | /**
374 | * To be rendered when the data has no data (Ex. filtered data)
375 | *
376 | * @return (node) An div with a message
377 | */
378 | noRowsRenderer () {
379 | return {this.props.messages.noData}
;
380 | }
381 |
382 | /**
383 | * Function called to get the content of each element of the list.
384 | *
385 | * @param (object) contentData
386 | * - index (integer) Index of the data to be rendered
387 | * - isScrolling (bool) If grid is scrollings
388 | * @return (node) element The element on the index position
389 | */
390 | rowRenderer(contentData) {
391 | return this.getContent(contentData);
392 | }
393 |
394 | /**
395 | * Function that gets the height for the current row of the list.
396 | *
397 | * @param (object) rowData It's an object that contains the index of the current row
398 | * @return (integer) rowHeight The height of each row.
399 | */
400 | getRowHeight(rowData) {
401 | let id = this.props.data.get(rowData.index).get(this.props.idField);
402 | return this.state.hiddenSelection.has(id) ? 0 : this.props.listRowHeight;
403 | }
404 |
405 | render(){
406 | let toolbar = null, rowHeight = this.props.listRowHeight, className = "proper-search-list";
407 |
408 | if (this.props.multiSelect) {
409 | toolbar = this.props.allowsEmptySelection ? this.getToolbarForEmpty() : this.getToolbar();
410 | }
411 |
412 | if (this.props.className) {
413 | className += ' '+this.props.className;
414 | }
415 |
416 | if (this.state.hiddenSelection.size > 0) {
417 | rowHeight = this.getRowHeight.bind(this);
418 | }
419 |
420 | return (
421 |
422 | {toolbar}
423 | {
425 | this._virtualScroll = ref
426 | }}
427 | className={"proper-search-list-virtual"}
428 | width={this.props.listWidth || this.props.containerWidth}
429 | height={this.props.listHeight}
430 | rowRenderer={this.rowRenderer.bind(this)}
431 | rowHeight={rowHeight}
432 | noRowsRenderer={this.noRowsRenderer.bind(this)}
433 | rowCount={this.props.data.size}
434 | overscanRowsCount={5}
435 | />
436 |
437 | )
438 | }
439 | }
440 |
441 | SearchList.defaultProps = getDefaultProps();
442 | let toExport = process.env.NODE_ENV === 'Test' ? SearchList : Dimensions()(SearchList)
443 | export default toExport;
--------------------------------------------------------------------------------
/src/jsx/lang/messages.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'SPA': {
3 | all: 'Seleccionar Todo',
4 | none: 'Deseleccionar Todo',
5 | empty: 'Seleccionar Vacios',
6 | notEmpty: 'Deseleccionar Vacios',
7 | loading: 'Cargando...',
8 | noData: 'No se encontró ningún elemento',
9 | errorIdField: 'No se pudo cambiar el `idField´, el campo',
10 | errorDisplayField: 'No se pudo cambiar el `displayField´, el campo',
11 | errorData: 'no existe en el array de datos o no ha cambiado',
12 | },
13 | 'ENG': {
14 | all: 'Select All',
15 | none: 'Unselect All',
16 | empty: 'Select Empty',
17 | notEmpty: 'Unselect Empty',
18 | loading: 'Loading...',
19 | noData:'No data found',
20 | errorIdField: "Couldn\'t change the `idField´, the field",
21 | errorDisplayField: "Couldn\'t change the `displayField´, the field",
22 | errorData: 'doesn\'t exist in the data array or has no changes',
23 | }
24 | }
--------------------------------------------------------------------------------
/src/jsx/lib/cache.js:
--------------------------------------------------------------------------------
1 | import dot from 'dot-object';
2 | import {map} from 'underscore';
3 | import merge from 'deepmerge';
4 |
5 | let cache = {};
6 |
7 | function parseKey(key) {
8 | return map(key, (k) => {
9 | return k.toString().replace('.', '_');
10 | }).join('.');
11 | }
12 |
13 | class RowCache {
14 | constructor(base = {}) {
15 | this.init(base);
16 | }
17 |
18 | init(base = null) {
19 | cache = base;
20 |
21 | return this;
22 | }
23 |
24 | read(key) {
25 | let k = parseKey(key);
26 | return dot.pick(k, cache);
27 | }
28 |
29 | write(key, value) {
30 | let k = parseKey(key);
31 | let writable = {};
32 |
33 | writable[k] = value;
34 | writable = dot.object(writable);
35 |
36 | cache = merge(cache, writable);
37 |
38 | return this;
39 | }
40 |
41 | flush(key = null) {
42 | if (key) {
43 | let k = parseKey(key);
44 | dot.remove(k, cache);
45 | } else {
46 | this.init();
47 | }
48 |
49 | return this;
50 | }
51 | }
52 |
53 | const rowcache = new RowCache();
54 |
55 | export default rowcache;
56 |
--------------------------------------------------------------------------------
/src/jsx/propersearch.js:
--------------------------------------------------------------------------------
1 | import Search from "./components/search";
2 |
3 | if (process.env.APP_ENV === 'browser') {
4 | require("../css/style.scss");
5 | }
6 |
7 | export default Search;
8 |
--------------------------------------------------------------------------------
/src/jsx/utils/normalize.js:
--------------------------------------------------------------------------------
1 | const charMap = {
2 | 'a': ['á','Á','à','À','ã','Ã','â','Â','ä','Ä','å','Å','ā','Ā','ą','Ą'],
3 | 'e': ['é','É','è','È','ê','Ê','ë','Ë','ē','Ē','ė','Ė','ę','Ę'],
4 | 'i': ['î','Î','í','Í','ì','Ì','ï','Ï','ī','Ī','į','Į'],
5 | 'l': ['ł', 'Ł'],
6 | 'o': ['ô','Ô','ò','Ò','ø','Ø','ō','Ō','ó','Ó','õ','Õ','ö','Ö'],
7 | 'u': ['û','Û','ú','Ú','ù','Ù','ü','Ü','ū','Ū'],
8 | 'c': ['ç','Ç','č','Č','ć','Ć'],
9 | 's': ['ś','Ś','š','Š'],
10 | 'z': ['ź','Ź','ż','Ż'],
11 | '' : ['@','#','~','$','!','º','|','"','·','%','&','¬','/','(',')','=','?','¿','¡','*','+','^','`','-','´','{','}','ç',';',':','.']
12 | }
13 |
14 | export default {
15 | normalize: function (value, parseToLower = true) {
16 | let rex = null;
17 |
18 | for(let element in charMap){
19 | rex = new RegExp('[' + charMap[element].toString() + ']', 'g');
20 |
21 | try{
22 | value = value.replace(rex, element);
23 | } catch(e) {
24 | console.log('error', value);
25 | }
26 | }
27 | return parseToLower ? value.toLowerCase() : value;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests.webpack.js:
--------------------------------------------------------------------------------
1 | // ES5 shims for Function.prototype.bind, Object.prototype.keys, etc.
2 | require('core-js/es5');
3 | // Replace ./src/js with the directory of your application code and
4 | // make sure the file name regexp matches your test files.
5 | var context = require.context('./src/jsx', true, /-test\.js$/);
6 | context.keys().forEach(context);
7 |
--------------------------------------------------------------------------------
/webpack.config.example.dist.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | context: path.join(__dirname, 'examples'),
6 | entry: {
7 | javascript: "./jsx/example.js"
8 | },
9 | module: {
10 | loaders: [
11 | {
12 | test: /\.js$/,
13 | exclude: /node_modules/,
14 | loaders: ["babel-loader"],
15 | },
16 | {
17 | test: /\.jsx$/,
18 | exclude: /node_modules/,
19 | loaders: ["babel-loader"],
20 | }
21 | ],
22 | },
23 | externals: {
24 | 'react': 'React',
25 | 'react-dom': 'ReactDOM',
26 | 'underscore': '_'
27 | },
28 | output: {
29 | libraryTarget: "var",
30 | library: "App",
31 | filename: "app.js",
32 | path: __dirname + "/examples/dist"
33 | },
34 | plugins: [
35 | new webpack.DefinePlugin({
36 | 'process.env': {
37 | NODE_ENV: JSON.stringify('production'),
38 | APP_ENV: JSON.stringify('example')
39 | },
40 | }),
41 | new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}})
42 | ]
43 | }
--------------------------------------------------------------------------------
/webpack.config.example.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 |
5 | const examplePath = path.join(__dirname, '/examples');
6 |
7 | module.exports = {
8 | context: path.join(__dirname, '/src'),
9 | entry: {
10 | javascript: path.join(examplePath, 'jsx/example.js'),
11 | html: path.join(examplePath, '/index.html')
12 | },
13 | module: {
14 | loaders: [
15 | {
16 | test: /\.js$/,
17 | exclude: /node_modules/,
18 | loaders: ["babel-loader"],
19 | },
20 | {
21 | test: /\.jsx$/,
22 | exclude: /node_modules/,
23 | loaders: ["babel-loader"],
24 | },
25 | {
26 | test: /\.html$/,
27 | loader: "file?name=[name].[ext]",
28 | exclude: /node_modules/
29 | },
30 | {
31 | test: /\.scss$/,
32 | loader: ExtractTextPlugin.extract('css!sass?includePaths[]='+path.resolve(__dirname, "./node_modules/compass-mixins/lib")),
33 | exclude: /node_modules/
34 | }
35 | ],
36 | },
37 | externals: {
38 | 'react': 'React',
39 | 'react-dom': 'ReactDOM',
40 | 'underscore': '_'
41 | },
42 | devtool: 'eval',
43 | output: {
44 | libraryTarget: "var",
45 | library: "ProperSearch",
46 | filename: "example.js",
47 | path: path.join(__dirname, "/dist")
48 | },
49 | plugins: [
50 | new ExtractTextPlugin('propersearch.css', {
51 | allChunks: true
52 | }),
53 | new webpack.optimize.DedupePlugin(),
54 | new webpack.DefinePlugin({
55 | 'process.env': {
56 | NODE_ENV: JSON.stringify('production'),
57 | APP_ENV: JSON.stringify('browser')
58 | },
59 | })
60 | ]
61 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 |
5 | module.exports = {
6 | context: path.join(__dirname, 'src'),
7 | entry: {
8 | javascript: "./jsx/propersearch.js"
9 | },
10 | module: {
11 | loaders: [
12 | {
13 | test: /\.js$/,
14 | exclude: /node_modules/,
15 | loaders: ["babel-loader"],
16 | },
17 | {
18 | test: /\.jsx$/,
19 | exclude: /node_modules/,
20 | loaders: ["babel-loader"],
21 | },
22 | {
23 | test: /\.html$/,
24 | loader: "file?name=[name].[ext]",
25 | },
26 | {
27 | test: /\.scss$/,
28 | loader: ExtractTextPlugin.extract('css!sass?includePaths[]='+path.resolve(__dirname, "./node_modules/compass-mixins/lib"))
29 | }
30 | ],
31 | },
32 | externals: {
33 | 'react': 'React',
34 | 'react-dom': 'ReactDOM',
35 | 'underscore': '_'
36 | },
37 | output: {
38 | libraryTarget: "var",
39 | library: "ProperSearch",
40 | filename: "propersearch.js",
41 | path: __dirname + "/dist"
42 | },
43 | plugins: [
44 | new ExtractTextPlugin('propersearch.css', {
45 | allChunks: true
46 | }),
47 | new webpack.optimize.DedupePlugin(),
48 | new webpack.DefinePlugin({
49 | 'process.env': {
50 | NODE_ENV: JSON.stringify('production'),
51 | APP_ENV: JSON.stringify('browser')
52 | },
53 | })
54 | ]
55 | }
--------------------------------------------------------------------------------
/webpack.config.min.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 |
5 | module.exports = {
6 | context: __dirname + '/src',
7 | entry: {
8 | javascript: "./jsx/propersearch.js"
9 | },
10 | module: {
11 | loaders: [
12 | {
13 | test: /\.js$/,
14 | exclude: /node_modules/,
15 | loaders: ["babel-loader"],
16 | },
17 | {
18 | test: /\.jsx$/,
19 | exclude: /node_modules/,
20 | loaders: ["babel-loader"],
21 | },
22 | {
23 | test: /\.html$/,
24 | loader: "file?name=[name].[ext]",
25 | },
26 | {
27 | test: /\.scss$/,
28 | loader: ExtractTextPlugin.extract('css!sass?includePaths[]='+path.resolve(__dirname, "./node_modules/compass-mixins/lib"))
29 | }
30 | ],
31 | },
32 | externals: {
33 | 'react': 'React',
34 | 'react-dom': 'ReactDOM',
35 | 'underscore': '_'
36 | },
37 | output: {
38 | libraryTarget: "var",
39 | library: "ProperSearch",
40 | filename: "propersearch.min.js",
41 | path: __dirname + "/dist"
42 | },
43 | plugins: [
44 | new ExtractTextPlugin('propersearch.min.css', {
45 | allChunks: true
46 | }),
47 | new webpack.optimize.DedupePlugin(),
48 | new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}),
49 | new webpack.DefinePlugin({
50 | 'process.env': {
51 | NODE_ENV: JSON.stringify('production'),
52 | APP_ENV: JSON.stringify('browser')
53 | },
54 | })
55 | ]
56 | }
--------------------------------------------------------------------------------