├── .babelrc ├── .eslintrc.js ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs ├── bundle.js ├── index.html └── styles.css ├── examples ├── example.scss ├── index.html ├── index.js ├── sampleData.json └── webpack.config.js ├── lib ├── bundle.js └── styles.css ├── package-lock.json ├── package.json ├── src ├── css │ └── tableFilter.scss ├── filterIcon.js ├── filterList.js ├── filterListItem.js ├── index.js ├── lib │ ├── constants.js │ ├── debounce.js │ ├── eventStack │ │ ├── eventStack.js │ │ ├── eventTarget.js │ │ ├── index.js │ │ └── normalizeTarget.js │ ├── filter.js │ ├── sort.js │ └── util.js ├── searchBar.js ├── selectAllItem.js ├── sortIcon.js └── template.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": "commonjs", 5 | "targets": { 6 | "node": "current" 7 | } 8 | }], 9 | ["@babel/preset-react"] 10 | ], 11 | "plugins": ["transform-object-assign", "@babel/plugin-proposal-class-properties"] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "google", 10 | "plugin:react/recommended" 11 | ], 12 | "parser": "babel-eslint", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 2018, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react", 22 | "babel" 23 | ], 24 | "rules": { 25 | "no-invalid-this": 0, 26 | "babel/no-invalid-this": 1, 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "16.7" 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | logs.log 3 | *.rdb 4 | *.log 5 | *.DS_STORE 6 | *.DS_Store 7 | stats.json 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at cheekujha@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cheekujha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-table-filter 2 | 3 | > Module creates Excel like Column Filters for Table. The filter list contains all the unique items present in every column. See image below for example. 4 | 5 | ![alt text](https://user-images.githubusercontent.com/13845950/34553583-42f475c0-f14e-11e7-87f0-7d9704545bb9.png) 6 | 7 | ## Demo 8 | 9 | [https://cheekujha.github.io/react-table-filter/](https://cheekujha.github.io/react-table-filter/) 10 | 11 | ## Install 12 | You need to have react and react-dom as dependencies in your project. 13 | 14 | 1. With [npm](https://npmjs.org/) installed, run 15 | 16 | ``` 17 | $ npm install react-table-filter --save 18 | ``` 19 | 20 | 2. At this point you can import react-table-filter and its styles in your application as follows: 21 | 22 | ```js 23 | import TableFilter from 'react-table-filter'; 24 | 25 | // Be sure to include styles at some point, probably during your bootstraping 26 | // In JS 27 | import react-table-filter/lib/styles.css; 28 | 29 | // In SCSS 30 | @import "path-to-node_modules/react-table-filter/lib/styles.css"; 31 | 32 | // Or directly import css 33 | 34 | 35 | ``` 36 | 37 | ## Usage 38 | 39 | 1. Wrap header columns (th / td) with TableFilter as shown below. 40 | ``` 41 | 44 | 45 | Name 46 | 47 | 48 | Season 49 | 50 | 51 | Number 52 | 53 | 54 | ``` 55 | Required Props on TableFilter 56 | 57 | rows - Initial Array of Items 58 | 59 | onFilterUpdate - Function called with updated filtered data when filters are added/removed. This function is used to show updated data by your application. Ex: 60 | 61 | ``` 62 | filterUpdated = (newData, filterConfiguration) => { 63 | this.setState({ 64 | "upddatedData": newData 65 | }); 66 | } 67 | ``` 68 | ``` 69 | Arguments Detail: 70 | newData - Filtered Data to be used to show on UI 71 | filterConfiguration - Current filters configuration. 72 | ``` 73 | **filterConfiguration** can be saved and be passed as prop(initialFilters) to **TableFilter** to maintain filter state while initializing the component.(In case user navigates away and comes back etc.) 74 | 75 | Required Props on th/td (Header columns) 76 | 77 | filterkey - The key by which that column is to be filtered(key as present in rows data) 78 | 79 | Only the Columns with "filterkey" prop present will be considered for filtering. 80 | 81 | ## Reset Items after Initialization 82 | 83 | If you want to reset Items after component mount. Make a reference to **TableFilter** node and call **reset** method as shown below. 84 | 85 | ``` 86 | {this.tableFilterNode = node;}> 91 | 92 | this.tableFilterNode.reset(newData, resetFilters); 93 | ``` 94 | 95 | ``` 96 | Arguments Detail: 97 | newData - Data to reset 98 | resetFilters(Default: true) - Boolean tells component to maintain/reset existing filters 99 | ``` 100 | ## API 101 | 102 | ### Properties 103 | 104 | #### TableFilter 105 | 106 | Name | Type | Default | Required | Description 107 | :--- | :--- | :------ | :------- | :---------- 108 | rows | array | | true | Items for the Filter 109 | onFilterUpdate | function(updatedData, filterConfiguration) | | true | Function called with filtered data and updated filter configuration 110 | rowClass | string | | false | Any additional class to be added to table row contaning header columns 111 | initialFilters | Array | | false | Initial Filter configuration to be applied. Configuration is received as second argument for **onFilterUpdate** function 112 | rowComponent | Component | Table Row Element | false | The columns headers will by default be placed inside Table Row Element. If **rowComponent** is passed it will be used. Any react component can be used. 113 | 114 | #### TableFilter Ref Methods 115 | 116 | Name | Type | Description 117 | :--- | :--- | :---------- 118 | reset | function(items, resetFilters=true) | Function called to reset items after component has been mounted. You can choose to either reset current filters or not. 119 | 120 | 121 | #### Cloumn Headers(td/th) 122 | 123 | Name | Type | Default | Required | Description 124 | :--- | :--- | :------ | :------- | :---------- 125 | filterkey | string | | false | Key by which the Column should be filtered(Key as present in single Item) 126 | itemDisplayValueFunc | function(itemValue) | | false | Optional Function that returns the Value that is displayed in the filter list for each item(Default is the item value - Item[key]) 127 | itemSortValueFunc | function(itemValue) | | false | Optional Function that returns the Value that is used while sorting (Default is the item value - Item[key]) 128 | alignleft | string | "false" | false | Decides while side filter list should be aligned w.r.t Column. Allowed "true"/"false" 129 | casesensitive | string | "false" | false | Case Sensitivity during sort. Allowed "true"/"false" 130 | showsearch | string | "true" | false | Display/Hide the search input. Allowed "true"/"false" 131 | 132 | ## License 133 | 134 | MIT 135 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Samples 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 3 | color: #222; } 4 | 5 | .nav-bar { 6 | background-color: #f8f8f8; 7 | border: 1px solid #e7e7e7; 8 | height: 50px; 9 | border-radius: 4px; } 10 | .nav-bar .container { 11 | padding-right: 15px; 12 | padding-left: 15px; 13 | margin-right: auto; 14 | margin-left: auto; 15 | line-height: 50px; 16 | color: #777; } 17 | .nav-bar:before { 18 | display: table; 19 | content: ""; } 20 | 21 | .examples { 22 | padding: 10px; 23 | box-sizing: border-box; 24 | position: relative; } 25 | .examples .basic { 26 | position: relative; 27 | border: 1px solid #d6d6d6; 28 | padding-bottom: 10px; } 29 | 30 | .header { 31 | position: relative; 32 | text-align: center; } 33 | 34 | .basic-table { 35 | position: relative; 36 | margin: auto; 37 | border: 1px solid black; 38 | border-collapse: collapse; 39 | width: 50%; } 40 | .basic-table .cell { 41 | position: relative; 42 | border: 1px solid black; 43 | padding: 3px; } 44 | .table-filter-row { 45 | position: relative; } 46 | 47 | .filter-list { 48 | position: absolute; 49 | max-height: 250px; 50 | overflow: auto; 51 | max-width: 220px; 52 | min-width: 170px; 53 | left: 0; 54 | top: 100%; 55 | border: 2px solid #8E9193; } 56 | .filter-list.align-left { 57 | left: auto; 58 | right: 0; } 59 | 60 | .filter-list-item { 61 | position: relative; 62 | padding-left: 30px; 63 | height: 35px; 64 | line-height: 35px; 65 | font-size: 14px; 66 | text-align: left; } 67 | .filter-list-item:nth-child(odd) { 68 | background-color: #F7F6F6; } 69 | .filter-list-item:nth-child(even) { 70 | background-color: white; } 71 | 72 | .filter-check-box { 73 | position: absolute; 74 | width: 14px; 75 | height: 14px; 76 | left: 8px; 77 | top: 50%; 78 | margin-top: -7px; 79 | box-sizing: border-box; 80 | overflow: hidden; 81 | border-radius: 2px; 82 | border: 1px solid #8F8F8F; } 83 | .filter-check-box.selected { 84 | border-color: black; 85 | background-color: black; } 86 | .filter-check-box.selected:after { 87 | content: ''; 88 | position: absolute; 89 | width: 8px; 90 | height: 5px; 91 | box-sizing: border-box; 92 | border-left: 2px solid white; 93 | border-bottom: 2px solid white; 94 | top: 50%; 95 | margin-top: -4px; 96 | left: 2px; 97 | -webkit-transform: rotate(-45deg); 98 | -moz-transform: rotate(-45deg); 99 | -ms-tranfsorm: rotate(-45deg); 100 | -o-transform: rotate(-45deg); 101 | transform: rotate(-45deg); } 102 | 103 | .filter-label { 104 | white-space: nowrap; 105 | overflow: hidden; 106 | text-overflow: ellipsis; 107 | position: relative; 108 | font-weight: normal; } 109 | .filter-label.select-all-label { 110 | font-weight: 600; } 111 | 112 | .apply-filter { 113 | position: relative; 114 | padding-right: 40px; } 115 | 116 | .table-filter-parent { 117 | position: absolute; 118 | right: 3px; 119 | top: 50%; 120 | margin-top: -8px; 121 | z-index: 10; } 122 | 123 | .table-filter-icon { 124 | position: relative; 125 | border-top: 8px solid gray; 126 | box-sizing: border-box; 127 | border-right: 6px solid transparent; 128 | border-left: 6px solid transparent; 129 | width: 0px; 130 | height: 0px; 131 | box-shadow: inset 0 4px gray; 132 | padding: 2px; } 133 | .table-filter-icon.selected { 134 | border-top-color: black; 135 | box-shadow: inset 0 4px black; } 136 | 137 | .ripple { 138 | position: relative; 139 | overflow: hidden; 140 | -webkit-transform: translate3d(0, 0, 0); 141 | -moz-transform: translate3d(0, 0, 0); 142 | -ms-tranfsorm: translate3d(0, 0, 0); 143 | -o-transform: translate3d(0, 0, 0); 144 | transform: translate3d(0, 0, 0); } 145 | 146 | .ripple:after { 147 | content: ""; 148 | display: block; 149 | position: absolute; 150 | width: 100%; 151 | height: 100%; 152 | top: 0; 153 | left: 0; 154 | pointer-events: none; 155 | background-image: radial-gradient(circle, #000 10%, transparent 10.01%); 156 | background-repeat: no-repeat; 157 | background-position: 50%; 158 | -webkit-transform: scale(10, 10); 159 | -moz-transform: scale(10, 10); 160 | -ms-tranfsorm: scale(10, 10); 161 | -o-transform: scale(10, 10); 162 | transform: scale(10, 10); 163 | opacity: 0; 164 | -webkit-transition: transform 0.5s, opacity 1s; 165 | -moz-transition: transform 0.5s, opacity 1s; 166 | -ms-transition: transform 0.5s, opacity 1s; 167 | -o-transition: transform 0.5s, opacity 1s; 168 | transition: transform 0.5s, opacity 1s; } 169 | 170 | .ripple:active:after { 171 | -webkit-transform: scale(0, 0); 172 | -moz-transform: scale(0, 0); 173 | -ms-tranfsorm: scale(0, 0); 174 | -o-transform: scale(0, 0); 175 | transform: scale(0, 0); 176 | opacity: .2; 177 | -webkit-transition: 0s; 178 | -moz-transition: 0s; 179 | -ms-transition: 0s; 180 | -o-transition: 0s; 181 | transition: 0s; } 182 | 183 | .sort-parent { 184 | position: absolute; 185 | background: #F0EEEE; 186 | z-index: 1; 187 | right: 6px; 188 | top: 6px; 189 | border-radius: 4px; 190 | border: 1px solid #E1DDDD; 191 | text-align: center; 192 | padding: 5px 15px; 193 | cursor: pointer; } 194 | .sort-parent.asc .table-filter-arrow.asc { 195 | background: black; } 196 | .sort-parent.asc .table-filter-arrow.asc:after { 197 | border-top-color: black; } 198 | .sort-parent.dsc .table-filter-arrow.dsc { 199 | background: black; } 200 | .sort-parent.dsc .table-filter-arrow.dsc:after { 201 | border-bottom-color: black; } 202 | 203 | .clear-fix:after { 204 | content: ""; 205 | display: table; 206 | clear: both; } 207 | 208 | .table-filter-arrow { 209 | position: relative; 210 | float: left; 211 | width: 2px; 212 | height: 12px; 213 | background: gray; } 214 | .table-filter-arrow.asc { 215 | margin-left: 7px; } 216 | .table-filter-arrow.asc:after { 217 | content: ""; 218 | position: absolute; 219 | border-top: 5px solid gray; 220 | bottom: -1px; 221 | border-left: 5px solid transparent; 222 | border-right: 5px solid transparent; 223 | left: -4px; } 224 | .table-filter-arrow.dsc:after { 225 | content: ""; 226 | position: absolute; 227 | border-bottom: 5px solid gray; 228 | top: -1px; 229 | border-left: 5px solid transparent; 230 | border-right: 5px solid transparent; 231 | left: -4px; } 232 | 233 | .search-parent { 234 | position: relative; 235 | width: 100%; 236 | box-sizing: border-box; 237 | padding-left: 8px; 238 | padding-right: 60px; } 239 | .search-parent .search-input { 240 | position: relative; 241 | width: 100%; 242 | height: 24px; 243 | margin: 0; 244 | padding-left: 5px; 245 | box-sizing: border-box; 246 | font-size: 14px; } 247 | -------------------------------------------------------------------------------- /examples/example.scss: -------------------------------------------------------------------------------- 1 | body{ 2 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 3 | color: #222; 4 | } 5 | 6 | .nav-bar{ 7 | background-color: #f8f8f8; 8 | border: 1px solid #e7e7e7; 9 | height: 50px; 10 | border-radius: 4px; 11 | .container{ 12 | padding-right: 15px; 13 | padding-left: 15px; 14 | margin-right: auto; 15 | margin-left: auto; 16 | line-height: 50px; 17 | color: #777; 18 | } 19 | &:before{ 20 | display: table; 21 | content: ""; 22 | } 23 | } 24 | 25 | .examples{ 26 | padding: 10px; 27 | box-sizing: border-box; 28 | position: relative; 29 | .basic{ 30 | position: relative; 31 | border: 1px solid rgb(214, 214, 214); 32 | padding-bottom: 10px; 33 | } 34 | } 35 | 36 | .header{ 37 | position: relative; 38 | text-align: center; 39 | } 40 | 41 | .basic-table{ 42 | position: relative; 43 | margin: auto; 44 | border: 1px solid black; 45 | border-collapse: collapse; 46 | width: 50%; 47 | 48 | .cell{ 49 | position: relative; 50 | border: 1px solid black; 51 | padding: 3px; 52 | } 53 | } -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Samples 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import SampleData from './sampleData.json'; 4 | import TableFilter from '../lib/bundle.js'; 5 | import {} from './example.scss'; 6 | import {} from '../lib/styles.css'; 7 | 8 | class SimpleExample extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | 'episodes': SampleData._embedded.episodes, 13 | }; 14 | this._filterUpdated = this._filterUpdated.bind(this); 15 | } 16 | 17 | _filterUpdated(newData, filtersObject) { 18 | this.setState({ 19 | 'episodes': newData, 20 | }); 21 | } 22 | 23 | render() { 24 | const episodes = this.state.episodes; 25 | const elementsHtml = episodes.map((item, index) => { 26 | return ( 27 | 28 | 29 | { item.name } 30 | 31 | 32 | { item.season } 33 | 34 | 35 | { item.number } 36 | 37 | 38 | ); 39 | }); 40 | return ( 41 |
42 |
43 |
44 | React Table Filters 45 |
46 |
47 |
48 |
49 |

Basic Usage

50 | 51 | 52 | 55 | 58 | 61 | 64 | 65 | 66 | 67 | { elementsHtml } 68 | 69 |
56 | Name 57 | 59 | Season 60 | 62 | Number 63 |
70 |
71 | 72 |
73 |
74 | ); 75 | } 76 | } 77 | 78 | ReactDOM.render( 79 | , 80 | document.getElementById('mainContainer') 81 | ); 82 | -------------------------------------------------------------------------------- /examples/sampleData.json: -------------------------------------------------------------------------------- 1 | { 2 | "id":82, 3 | "url":"http://www.tvmaze.com/shows/82/game-of-thrones", 4 | "name":"Game of Thrones", 5 | "type":"Scripted", 6 | "language":"English", 7 | "genres":[ 8 | "Drama", 9 | "Adventure", 10 | "Fantasy" 11 | ], 12 | "status":"Running", 13 | "runtime":60, 14 | "premiered":"2011-04-17", 15 | "officialSite":"http://www.hbo.com/game-of-thrones", 16 | "schedule":{ 17 | "time":"21:00", 18 | "days":[ 19 | "Sunday" 20 | ] 21 | }, 22 | "rating":{ 23 | "average":9.3 24 | }, 25 | "weight":100, 26 | "network":{ 27 | "id":8, 28 | "name":"HBO", 29 | "country":{ 30 | "name":"United States", 31 | "code":"US", 32 | "timezone":"America/New_York" 33 | } 34 | }, 35 | "webChannel":{ 36 | "id":22, 37 | "name":"HBO Go", 38 | "country":{ 39 | "name":"United States", 40 | "code":"US", 41 | "timezone":"America/New_York" 42 | } 43 | }, 44 | "externals":{ 45 | "tvrage":24493, 46 | "thetvdb":121361, 47 | "imdb":"tt0944947" 48 | }, 49 | "image":{ 50 | "medium":"http://static.tvmaze.com/uploads/images/medium_portrait/124/310209.jpg", 51 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/124/310209.jpg" 52 | }, 53 | "summary":"

Based on the bestselling book series A Song of Ice and Fire by George R.R. Martin, this sprawling new HBO drama is set in a world where summers span decades and winters can last a lifetime. From the scheming south and the savage eastern lands, to the frozen north and ancient Wall that protects the realm from the mysterious darkness beyond, the powerful families of the Seven Kingdoms are locked in a battle for the Iron Throne. This is a story of duplicity and treachery, nobility and honor, conquest and triumph. In the Game of Thrones, you either win or you die.

", 54 | "updated":1506726326, 55 | "_links":{ 56 | "self":{ 57 | "href":"http://api.tvmaze.com/shows/82" 58 | }, 59 | "previousepisode":{ 60 | "href":"http://api.tvmaze.com/episodes/1221415" 61 | } 62 | }, 63 | "_embedded":{ 64 | "episodes":[ 65 | { 66 | "id":4952, 67 | "url":"http://www.tvmaze.com/episodes/4952/game-of-thrones-1x01-winter-is-coming", 68 | "name":"Winter is Coming", 69 | "season":1, 70 | "number":1, 71 | "airdate":"2011-04-17", 72 | "airtime":"21:00", 73 | "airstamp":"2011-04-18T01:00:00+00:00", 74 | "runtime":60, 75 | "image":{ 76 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2668.jpg", 77 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2668.jpg" 78 | }, 79 | "summary":"

Lord Eddard Stark, ruler of the North, is summoned to court by his old friend, King Robert Baratheon, to serve as the King's Hand. Eddard reluctantly agrees after learning of a possible threat to the King's life. Eddard's bastard son Jon Snow must make a painful decision about his own future, while in the distant east Viserys Targaryen plots to reclaim his father's throne, usurped by Robert, by selling his sister in marriage.

", 80 | "_links":{ 81 | "self":{ 82 | "href":"http://api.tvmaze.com/episodes/4952" 83 | } 84 | } 85 | }, 86 | { 87 | "id":4953, 88 | "url":"http://www.tvmaze.com/episodes/4953/game-of-thrones-1x02-the-kingsroad", 89 | "name":"The Kingsroad", 90 | "season":1, 91 | "number":2, 92 | "airdate":"2011-04-24", 93 | "airtime":"21:00", 94 | "airstamp":"2011-04-25T01:00:00+00:00", 95 | "runtime":60, 96 | "image":{ 97 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2669.jpg", 98 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2669.jpg" 99 | }, 100 | "summary":"

An incident on the Kingsroad threatens Eddard and Robert's friendship. Jon and Tyrion travel to the Wall, where they discover that the reality of the Night's Watch may not match the heroic image of it.

", 101 | "_links":{ 102 | "self":{ 103 | "href":"http://api.tvmaze.com/episodes/4953" 104 | } 105 | } 106 | }, 107 | { 108 | "id":4954, 109 | "url":"http://www.tvmaze.com/episodes/4954/game-of-thrones-1x03-lord-snow", 110 | "name":"Lord Snow", 111 | "season":1, 112 | "number":3, 113 | "airdate":"2011-05-01", 114 | "airtime":"21:00", 115 | "airstamp":"2011-05-02T01:00:00+00:00", 116 | "runtime":60, 117 | "image":{ 118 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2671.jpg", 119 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2671.jpg" 120 | }, 121 | "summary":"

Jon Snow attempts to find his place amongst the Night's Watch. Eddard and his daughters arrive at King's Landing.

", 122 | "_links":{ 123 | "self":{ 124 | "href":"http://api.tvmaze.com/episodes/4954" 125 | } 126 | } 127 | }, 128 | { 129 | "id":4955, 130 | "url":"http://www.tvmaze.com/episodes/4955/game-of-thrones-1x04-cripples-bastards-and-broken-things", 131 | "name":"Cripples, Bastards, and Broken Things", 132 | "season":1, 133 | "number":4, 134 | "airdate":"2011-05-08", 135 | "airtime":"21:00", 136 | "airstamp":"2011-05-09T01:00:00+00:00", 137 | "runtime":60, 138 | "image":{ 139 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2673.jpg", 140 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2673.jpg" 141 | }, 142 | "summary":"

Tyrion stops at Winterfell on his way home and gets a frosty reception from Robb Stark. Eddard's investigation into the death of his predecessor gets underway.

", 143 | "_links":{ 144 | "self":{ 145 | "href":"http://api.tvmaze.com/episodes/4955" 146 | } 147 | } 148 | }, 149 | { 150 | "id":4956, 151 | "url":"http://www.tvmaze.com/episodes/4956/game-of-thrones-1x05-the-wolf-and-the-lion", 152 | "name":"The Wolf and the Lion", 153 | "season":1, 154 | "number":5, 155 | "airdate":"2011-05-15", 156 | "airtime":"21:00", 157 | "airstamp":"2011-05-16T01:00:00+00:00", 158 | "runtime":60, 159 | "image":{ 160 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2674.jpg", 161 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2674.jpg" 162 | }, 163 | "summary":"

Catelyn's actions on the road have repercussions for Eddard. Tyrion enjoys the dubious hospitality of the Eyrie.

", 164 | "_links":{ 165 | "self":{ 166 | "href":"http://api.tvmaze.com/episodes/4956" 167 | } 168 | } 169 | }, 170 | { 171 | "id":4957, 172 | "url":"http://www.tvmaze.com/episodes/4957/game-of-thrones-1x06-a-golden-crown", 173 | "name":"A Golden Crown", 174 | "season":1, 175 | "number":6, 176 | "airdate":"2011-05-22", 177 | "airtime":"21:00", 178 | "airstamp":"2011-05-23T01:00:00+00:00", 179 | "runtime":60, 180 | "image":{ 181 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2676.jpg", 182 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2676.jpg" 183 | }, 184 | "summary":"

Viserys is increasingly frustrated by the lack of progress towards gaining his crown.

", 185 | "_links":{ 186 | "self":{ 187 | "href":"http://api.tvmaze.com/episodes/4957" 188 | } 189 | } 190 | }, 191 | { 192 | "id":4958, 193 | "url":"http://www.tvmaze.com/episodes/4958/game-of-thrones-1x07-you-win-or-you-die", 194 | "name":"You Win or You Die", 195 | "season":1, 196 | "number":7, 197 | "airdate":"2011-05-29", 198 | "airtime":"21:00", 199 | "airstamp":"2011-05-30T01:00:00+00:00", 200 | "runtime":60, 201 | "image":{ 202 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2677.jpg", 203 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2677.jpg" 204 | }, 205 | "summary":"

Eddard's investigations in King's Landing reach a climax and a dark secret is revealed.

", 206 | "_links":{ 207 | "self":{ 208 | "href":"http://api.tvmaze.com/episodes/4958" 209 | } 210 | } 211 | }, 212 | { 213 | "id":4959, 214 | "url":"http://www.tvmaze.com/episodes/4959/game-of-thrones-1x08-the-pointy-end", 215 | "name":"The Pointy End", 216 | "season":1, 217 | "number":8, 218 | "airdate":"2011-06-05", 219 | "airtime":"21:00", 220 | "airstamp":"2011-06-06T01:00:00+00:00", 221 | "runtime":60, 222 | "image":{ 223 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2678.jpg", 224 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2678.jpg" 225 | }, 226 | "summary":"

Tyrion joins his father's army with unexpected allies. Events in King's Landing take a turn for the worse as Arya's lessons are put to the test.

", 227 | "_links":{ 228 | "self":{ 229 | "href":"http://api.tvmaze.com/episodes/4959" 230 | } 231 | } 232 | }, 233 | { 234 | "id":4960, 235 | "url":"http://www.tvmaze.com/episodes/4960/game-of-thrones-1x09-baelor", 236 | "name":"Baelor", 237 | "season":1, 238 | "number":9, 239 | "airdate":"2011-06-12", 240 | "airtime":"21:00", 241 | "airstamp":"2011-06-13T01:00:00+00:00", 242 | "runtime":60, 243 | "image":{ 244 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2679.jpg", 245 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2679.jpg" 246 | }, 247 | "summary":"

Catelyn must negotiate with the irascible Lord Walder Frey.

", 248 | "_links":{ 249 | "self":{ 250 | "href":"http://api.tvmaze.com/episodes/4960" 251 | } 252 | } 253 | }, 254 | { 255 | "id":4961, 256 | "url":"http://www.tvmaze.com/episodes/4961/game-of-thrones-1x10-fire-and-blood", 257 | "name":"Fire and Blood", 258 | "season":1, 259 | "number":10, 260 | "airdate":"2011-06-19", 261 | "airtime":"21:00", 262 | "airstamp":"2011-06-20T01:00:00+00:00", 263 | "runtime":60, 264 | "image":{ 265 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/2681.jpg", 266 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/2681.jpg" 267 | }, 268 | "summary":"

Daenerys must realize her destiny. Jaime finds himself in an unfamiliar predicament.

", 269 | "_links":{ 270 | "self":{ 271 | "href":"http://api.tvmaze.com/episodes/4961" 272 | } 273 | } 274 | }, 275 | { 276 | "id":4962, 277 | "url":"http://www.tvmaze.com/episodes/4962/game-of-thrones-2x01-the-north-remembers", 278 | "name":"The North Remembers", 279 | "season":2, 280 | "number":1, 281 | "airdate":"2012-04-01", 282 | "airtime":"21:00", 283 | "airstamp":"2012-04-02T01:00:00+00:00", 284 | "runtime":60, 285 | "image":{ 286 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3174.jpg", 287 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3174.jpg" 288 | }, 289 | "summary":"

War grips the continent of Westeros. As Tyrion Lannister tries to take his strong-willed nephew in hand in King's Landing, Stannis Baratheon launches his own campaign to take the Iron Throne with the help of a mysterious priestess. In the east, Daenerys must lead her retinue through a desolate wasteland whilst beyond the Wall the Night's Watch seeks the aid of a wildling.

", 290 | "_links":{ 291 | "self":{ 292 | "href":"http://api.tvmaze.com/episodes/4962" 293 | } 294 | } 295 | }, 296 | { 297 | "id":4963, 298 | "url":"http://www.tvmaze.com/episodes/4963/game-of-thrones-2x02-the-night-lands", 299 | "name":"The Night Lands", 300 | "season":2, 301 | "number":2, 302 | "airdate":"2012-04-08", 303 | "airtime":"21:00", 304 | "airstamp":"2012-04-09T01:00:00+00:00", 305 | "runtime":60, 306 | "image":{ 307 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3175.jpg", 308 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3175.jpg" 309 | }, 310 | "summary":"

Stannis uses Ser Davos to seek out new allies for his war with the Lannisters. On the road north, Arya confides in Gendry. Robb Stark sends Theon Greyjoy to win an alliance with his father and the fierce warriors of the Iron Islands. Cersei and Tyrion clash on how to rule in King's Landing.

", 311 | "_links":{ 312 | "self":{ 313 | "href":"http://api.tvmaze.com/episodes/4963" 314 | } 315 | } 316 | }, 317 | { 318 | "id":4964, 319 | "url":"http://www.tvmaze.com/episodes/4964/game-of-thrones-2x03-what-is-dead-may-never-die", 320 | "name":"What is Dead May Never Die", 321 | "season":2, 322 | "number":3, 323 | "airdate":"2012-04-15", 324 | "airtime":"21:00", 325 | "airstamp":"2012-04-16T01:00:00+00:00", 326 | "runtime":60, 327 | "image":{ 328 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3176.jpg", 329 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3176.jpg" 330 | }, 331 | "summary":"

Catelyn Stark treats with King Renly in the hope of winning an alliance. Tyrion undertakes a complex plan in King's Landing to expose an enemy. At Winterfell, Bran's dreams continue to trouble him.

", 332 | "_links":{ 333 | "self":{ 334 | "href":"http://api.tvmaze.com/episodes/4964" 335 | } 336 | } 337 | }, 338 | { 339 | "id":4965, 340 | "url":"http://www.tvmaze.com/episodes/4965/game-of-thrones-2x04-garden-of-bones", 341 | "name":"Garden of Bones", 342 | "season":2, 343 | "number":4, 344 | "airdate":"2012-04-22", 345 | "airtime":"21:00", 346 | "airstamp":"2012-04-23T01:00:00+00:00", 347 | "runtime":60, 348 | "image":{ 349 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3177.jpg", 350 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3177.jpg" 351 | }, 352 | "summary":"

Tyrion attempts to restrain Joffrey's cruelty. Catelyn attempts to broker a peace between Stannis and Renly. Daenerys and her followers arrive at the great city of Qarth and hope to find refuge there. Arya and Gendry arrive at Harrenhal, a great castle now under Lannister occupation.

", 353 | "_links":{ 354 | "self":{ 355 | "href":"http://api.tvmaze.com/episodes/4965" 356 | } 357 | } 358 | }, 359 | { 360 | "id":4966, 361 | "url":"http://www.tvmaze.com/episodes/4966/game-of-thrones-2x05-the-ghost-of-harrenhal", 362 | "name":"The Ghost of Harrenhal", 363 | "season":2, 364 | "number":5, 365 | "airdate":"2012-04-29", 366 | "airtime":"21:00", 367 | "airstamp":"2012-04-30T01:00:00+00:00", 368 | "runtime":60, 369 | "image":{ 370 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3178.jpg", 371 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3178.jpg" 372 | }, 373 | "summary":"

Confusion rages in the Stormlands in the wake of a devastating reversal. Catelyn must flee with a new ally, whilst Littlefinger sees an opportunity in the chaos. Theon seeks to prove himself to his father in battle. Arya receives a promise from the enigmatic Jaqen H'ghar. The Night's Watch arrives at the Fist of the First Men. Daenerys Targaryen receives a marriage proposal.

", 374 | "_links":{ 375 | "self":{ 376 | "href":"http://api.tvmaze.com/episodes/4966" 377 | } 378 | } 379 | }, 380 | { 381 | "id":4967, 382 | "url":"http://www.tvmaze.com/episodes/4967/game-of-thrones-2x06-the-old-gods-and-the-new", 383 | "name":"The Old Gods and the New", 384 | "season":2, 385 | "number":6, 386 | "airdate":"2012-05-06", 387 | "airtime":"21:00", 388 | "airstamp":"2012-05-07T01:00:00+00:00", 389 | "runtime":60, 390 | "image":{ 391 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3180.jpg", 392 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3180.jpg" 393 | }, 394 | "summary":"

Arya has a surprise visitor; Dany vows to take what is hers; Joffrey meets his subjects; Qhorin gives Jon a chance to prove himself.

", 395 | "_links":{ 396 | "self":{ 397 | "href":"http://api.tvmaze.com/episodes/4967" 398 | } 399 | } 400 | }, 401 | { 402 | "id":4968, 403 | "url":"http://www.tvmaze.com/episodes/4968/game-of-thrones-2x07-a-man-without-honor", 404 | "name":"A Man Without Honor", 405 | "season":2, 406 | "number":7, 407 | "airdate":"2012-05-13", 408 | "airtime":"21:00", 409 | "airstamp":"2012-05-14T01:00:00+00:00", 410 | "runtime":60, 411 | "image":{ 412 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3192.jpg", 413 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3192.jpg" 414 | }, 415 | "summary":"

Jaime meets a relative; Theon hunts; Dany receives an invitation.

", 416 | "_links":{ 417 | "self":{ 418 | "href":"http://api.tvmaze.com/episodes/4968" 419 | } 420 | } 421 | }, 422 | { 423 | "id":4969, 424 | "url":"http://www.tvmaze.com/episodes/4969/game-of-thrones-2x08-the-prince-of-winterfell", 425 | "name":"The Prince of Winterfell", 426 | "season":2, 427 | "number":8, 428 | "airdate":"2012-05-20", 429 | "airtime":"21:00", 430 | "airstamp":"2012-05-21T01:00:00+00:00", 431 | "runtime":60, 432 | "image":{ 433 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3194.jpg", 434 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3194.jpg" 435 | }, 436 | "summary":"

Theon holds the fort; Arya calls in her debt with Jaqen; Robb is betrayed; Stannis and Davos approach their destination.

", 437 | "_links":{ 438 | "self":{ 439 | "href":"http://api.tvmaze.com/episodes/4969" 440 | } 441 | } 442 | }, 443 | { 444 | "id":4970, 445 | "url":"http://www.tvmaze.com/episodes/4970/game-of-thrones-2x09-blackwater", 446 | "name":"Blackwater", 447 | "season":2, 448 | "number":9, 449 | "airdate":"2012-05-27", 450 | "airtime":"21:00", 451 | "airstamp":"2012-05-28T01:00:00+00:00", 452 | "runtime":60, 453 | "image":{ 454 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3196.jpg", 455 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3196.jpg" 456 | }, 457 | "summary":"

A massive battle rages for control of King's Landing and the Iron Throne.

", 458 | "_links":{ 459 | "self":{ 460 | "href":"http://api.tvmaze.com/episodes/4970" 461 | } 462 | } 463 | }, 464 | { 465 | "id":4971, 466 | "url":"http://www.tvmaze.com/episodes/4971/game-of-thrones-2x10-valar-morghulis", 467 | "name":"Valar Morghulis", 468 | "season":2, 469 | "number":10, 470 | "airdate":"2012-06-03", 471 | "airtime":"21:00", 472 | "airstamp":"2012-06-04T01:00:00+00:00", 473 | "runtime":60, 474 | "image":{ 475 | "medium":"http://static.tvmaze.com/uploads/images/medium_landscape/1/3197.jpg", 476 | "original":"http://static.tvmaze.com/uploads/images/original_untouched/1/3197.jpg" 477 | }, 478 | "summary":"

Tyrion awakens to a changed situation. King Joffrey doles out rewards to his subjects. As Theon stirs his men to action, Luwin offers some final advice. Brienne silences Jaime; Arya receives a gift from Jaqen; Dany goes to a strange place; Jon proves himself to Qhorin.

", 479 | "_links":{ 480 | "self":{ 481 | "href":"http://api.tvmaze.com/episodes/4971" 482 | } 483 | } 484 | } 485 | ] 486 | } 487 | } -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'none', 7 | entry: './index.js', 8 | output: { 9 | filename: 'bundle.js', 10 | path: path.resolve(__dirname, '../docs') 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | // exclude: /(node_modules|bower_components)/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | "presets": [ 21 | [ 22 | "@babel/preset-env", 23 | { 24 | "modules": "commonjs", 25 | "targets": { 26 | "node": "current" 27 | } 28 | } 29 | ], 30 | "@babel/preset-react" 31 | ], 32 | "plugins": [ 33 | [ 34 | "@babel/plugin-proposal-class-properties",{"loose": true} 35 | ] 36 | ] 37 | } 38 | } 39 | }, 40 | { 41 | test: /\.scss$/, 42 | use: ExtractTextPlugin.extract({ 43 | fallback: 'style-loader', 44 | use: ['css-loader', 'sass-loader'] 45 | // use: ['css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'sass-loader'] 46 | }) 47 | }, 48 | { 49 | test: /\.css$/, 50 | use: ExtractTextPlugin.extract({ 51 | fallback: 'style-loader', 52 | use: ['css-loader'] 53 | // use: ['css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'sass-loader'] 54 | }) 55 | } 56 | ] 57 | }, 58 | plugins: [ 59 | new ExtractTextPlugin({filename: 'styles.css', disable: false, allChunks: true}), 60 | new HtmlWebpackPlugin({ 61 | filename: '../docs/index.html', 62 | template: 'index.html' 63 | }) 64 | ] 65 | }; 66 | -------------------------------------------------------------------------------- /lib/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.tableFilter=t(require("react")):e.tableFilter=t(e.React)}(window,(function(e){return function(e){var t={};function i(s){if(t[s])return t[s].exports;var r=t[s]={i:s,l:!1,exports:{}};return e[s].call(r.exports,r,r.exports,i),r.l=!0,r.exports}return i.m=e,i.c=t,i.d=function(e,t,s){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(i.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)i.d(s,r,function(t){return e[t]}.bind(null,r));return s},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=6)}([function(t,i){t.exports=e},function(e,t,i){e.exports=i(10)()},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.without=t.uniq=t.getValForKey=t.isTypeString=t.isTypeArray=t.isUndefined=void 0;const s=(e,t)=>null==e||"undefined"===e||"null"===e||!(!t||"string"!=typeof e||0!==e.toString().trim().length);t.isUndefined=s;const r=e=>"[object Array]"===Object.prototype.toString.call(e);t.isTypeArray=r;const l=e=>"[object String]"===Object.prototype.toString.call(e);t.isTypeString=l;const n=(e,t)=>{if(!s(t)){if(l(t)){const i=t.split(".");if(1===i.length)return e[t];{let t,r,l=e;for(t=0,r=i.length;t{if(null!=e&&e.length){return[...new Set(e)]}return[]};t.uniq=a;const o=(e,t=[])=>{const i=[];return e.length?(e.forEach(e=>{t.indexOf(e)<0&&i.push(e)}),i):i};t.without=o;var d={isUndefined:s,isTypeArray:r,isTypeString:l,getValForKey:n,uniq:a,without:o};t.default=d},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.DSC_VALUE=t.ASC_VALUE=t.BLANK_LABEL=void 0;t.BLANK_LABEL="(blank)";t.ASC_VALUE="asc";t.DSC_VALUE="dsc";var s={BLANK_LABEL:"(blank)",ASC_VALUE:"asc",DSC_VALUE:"dsc"};t.default=s},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.calculateFilterProps=t.createFiltersFromItems=t.filtersReset=t.filterAction=t.filterActions=void 0;var s=i(2),r=i(5),l=i(3);function n(){return(n=Object.assign||function(e){for(var t=1;t{const l=[],a=e.map(e=>{const a=n({},e);let o,d;for((0,s.isUndefined)(a.appliedFilters)&&(a.appliedFilters={}),o=0,d=t.length;o{const l=t.key;let a=t.value;if((0,s.isUndefined)(a)&&(a=""),!(0,s.isUndefined)(l)){const t=[],o=e.map(e=>{const o=n({},e);let d=(0,s.getValForKey)(e,l);return(0,s.isUndefined)(r)||(d=r(d)),(0,s.isUndefined)(d)&&(d=""),(0,s.isUndefined)(o.appliedFilters)&&(o.appliedFilters={}),(0,s.isTypeString)(d)&&(d=d.trim()),i?d===a&&(o.appliedFilters[l]||(o.appliedFilters[l]=0),o.appliedFilters[l]+=1):d===a&&(o.appliedFilters[l]-=1,0===o.appliedFilters[l]&&delete o.appliedFilters[l]),0===Object.keys(o.appliedFilters).length&&(delete o.appliedFilters,t.push(n({},o))),o});return{filteredArray:t,dataWithFilter:o}}};t.filtersReset=(e=[],t=[],i,r=!0,l)=>{const a=[],o=e.map(e=>{const o=n({},e);(0,s.isUndefined)(o.appliedFilters)&&(o.appliedFilters={});let d=(0,s.getValForKey)(o,i);return(0,s.isUndefined)(l)||(d=l(d)),(0,s.isUndefined)(d)&&(d=""),(0,s.isTypeString)(d)&&(d=d.trim()),t.indexOf(d)>=0&&(r?delete o.appliedFilters[i]:(o.appliedFilters[i]||(o.appliedFilters[i]=0),o.appliedFilters[i]++)),0===Object.keys(o.appliedFilters).length&&(delete o.appliedFilters,a.push(n({},o))),o});return{filteredArray:a,dataWithFilter:o}};const a=(e,t,i,a)=>{const o=e?[...e]:[],d=[];let u=[],c=!0;return o.map(e=>{let r=(0,s.getValForKey)(e,t),a=r;(0,s.isUndefined)(i)||(r=i(r));const o=e.appliedFilters||{};let p=r;if((0,s.isUndefined)(r)?(p=l.BLANK_LABEL,r="",a=p):(0,s.isTypeString)(r)&&(r=r.trim(),0===r.length&&(p=l.BLANK_LABEL,a=p)),-1===d.indexOf(r))!(0,s.isUndefined)(o)&&Object.keys(o).length>0?1===Object.keys(o).length&&Object.keys(o)[0]===t?(c=!1,u.push({key:r,display:p,selected:!1,visible:!0,orinigalValue:a})):u.push({key:r,display:p,selected:!0,visible:!1,orinigalValue:a}):u.push({key:r,display:p,selected:!0,visible:!0,orinigalValue:a}),d.push(r);else{const e=d.indexOf(r);let i=u[e];0===Object.keys(o).length&&(i.selected&&i.visible||(i=n({},i,{selected:!0,visible:!0}),u[e]=i)),1===Object.keys(o).length&&Object.keys(o)[0]===t&&(c=!1,i=n({},i,{selected:!1,visible:!0}),u[e]=i)}}),u=(0,r.sortAction)(u,l.ASC_VALUE,{valueFunc:a,key:"orinigalValue"}),{filterList:u,selectState:c}};t.createFiltersFromItems=a;t.calculateFilterProps=({filteredData:e,filterkey:t,itemDisplayValueFunc:i,itemSortValueFunc:r,sortKey:l,sortType:n})=>{const{filterList:o,selectState:d}=a(e,t,i,r);return{filterList:o,selectAllFilters:d,sortType:(0,s.isUndefined)(l)||l!==t?void 0:n}}},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.sortAction=void 0;var s=i(2);const r=(e=[],t,{valueFunc:i,caseSensitive:r=!1,key:l}={})=>{if(!(0,s.isUndefined)(t)){const n=(e,n)=>{let a,o;(0,s.isUndefined)(l)?(a=e,o=n):(a=(0,s.getValForKey)(e,l),o=(0,s.getValForKey)(n,l)),(0,s.isUndefined)(i)?(isNaN(Number(a))||isNaN(Number(o))||(a=Number(a),o=Number(o)),(0,s.isTypeString)(a)&&(a=a.trim(),r||(a=a.toUpperCase())),(0,s.isTypeString)(o)&&(o=o.trim(),r||(o=o.toUpperCase()))):(a=i(a),o=i(o)),(0,s.isUndefined)(a)&&(a=""),(0,s.isUndefined)(o)&&(o="");let d=0;return d=a{const t=this.currentFilters;if(!(0,l.isUndefined)(t)&&Object.keys(t).length>0){let i;Object.keys(t).map(s=>{const r=t[s].map(e=>({key:s,value:e})),l=(0,n.filterActions)(e,r,!0,this._getValueFunctionForKey(s));i=l.filteredArray,e=l.dataWithFilter}),this.props.onFilterUpdate&&this.props.onFilterUpdate(i,t)}return e}),c(this,"_getValueFunctionForKey",e=>{let t;return this.props.children.map((i,s)=>{(0,l.isUndefined)(i)||(0,l.isUndefined)(i.props.filterkey,!0)||i.props.filterkey!==e||(t=i.props.itemDisplayValueFunc)}),t}),c(this,"_createData",(e=[])=>{const t=[],i=[];return e.map(e=>{t.push(u({},e)),i.push(u({},e))}),{initialData:t,filteredData:i}}),c(this,"_filterMulipleRows",(e=[],t=[],i)=>{const s=this.state.filteredData;if(!(0,l.isUndefined)(e)){t.map(e=>{this._updateCurrentFilter(e.value,!1,e.key)}),e.map(e=>{this._updateCurrentFilter(e.value,!0,e.key)});let r=(0,n.filterActions)(s,t,!1,i);if(r=(0,n.filterActions)(r.dataWithFilter,e,!0,i),!(0,l.isUndefined)(r)){const e=r.filteredArray,t=r.dataWithFilter;this.setState({filteredData:t}),this.props.onFilterUpdate&&this.props.onFilterUpdate(e,this._getCurrentFilters())}}}),c(this,"_filterRows",(e,t,i=!0,s)=>{const r=this.state.filteredData;if(!(0,l.isUndefined)(e)&&!(0,l.isUndefined)(t)){this._updateCurrentFilters([e],i,t);const a=(0,n.filterAction)(r,{key:t,value:e},i,s);if(!(0,l.isUndefined)(a)){const e=a.filteredArray,t=a.dataWithFilter;this.setState({filteredData:t}),this.props.onFilterUpdate&&this.props.onFilterUpdate(e,this._getCurrentFilters())}}}),c(this,"_updateCurrentFilter",(e,t=!0,i)=>{if(!(0,l.isUndefined)(i,!0)&&!(0,l.isUndefined)(e,!0))if((0,l.isUndefined)(this.currentFilters[i])&&(this.currentFilters[i]=[]),t)this.currentFilters[i].indexOf(e)<0&&this.currentFilters[i].push(e);else if(this.currentFilters[i].indexOf(e)>=0){const t=this.currentFilters[i].indexOf(e);this.currentFilters[i]=[...this.currentFilters[i].slice(0,t),...this.currentFilters[i].slice(t+1)]}}),c(this,"_updateCurrentFilters",(e=[],t=!0,i)=>{(0,l.isUndefined)(e)||(0,l.isUndefined)(i)||e.map(e=>{this._updateCurrentFilter(e,t,i)})}),c(this,"_getCurrentFilters",()=>this.currentFilters),c(this,"_resetRows",(e=[],t,i=!0,s)=>{if(!(0,l.isUndefined)(t)){const r=this.state.filteredData;this._updateCurrentFilters(e,!i,t);const a=(0,n.filtersReset)(r,e,t,i,s);if(!(0,l.isUndefined)(a)){const e=a.filteredArray,t=a.dataWithFilter;this.setState({filteredData:t}),this.props.onFilterUpdate&&this.props.onFilterUpdate(e,this._getCurrentFilters())}}}),c(this,"_sortRows",(e,{valueFunc:t,caseSensitive:i=!1,key:s}={})=>{if(!(0,l.isUndefined)(e)){const r=this.state.filteredData,n=(0,a.sortAction)(r,e,{valueFunc:t,caseSensitive:i,key:s}),o=[];this.setState({filteredData:n,sortKey:s,sortType:e}),n.map(e=>{if((0,l.isUndefined)(e.appliedFilters)||0===Object.keys(e.appliedFilters).length){const t=u({},e);delete t.appliedFilters,o.push(t)}}),this.props.onFilterUpdate&&this.props.onFilterUpdate(o,this._getCurrentFilters())}}),c(this,"reset",(e,t=!0)=>{t?this.currentFilters={}:e=this._applyInitialFilters(e);const i=this._createData(e);this.setState({initialData:i.initialData,filteredData:i.filteredData})}),this.currentFilters=this.props.initialFilters||{};const t=this._applyInitialFilters(this.props.rows),i=this._createData(t);this.state={initialData:i.initialData,filteredData:i.filteredData,sortKey:void 0}}render(){return r.default.call(this)}}p.propTypes={rows:o.default.array.isRequired,onFilterUpdate:o.default.func.isRequired,rowClass:o.default.string,initialFilters:o.default.array,rowComponent:o.default.func,children:o.default.any};var f=p;t.default=f},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=a(i(0)),r=a(i(8)),l=i(2),n=(a(i(20)),i(4));function a(e){return e&&e.__esModule?e:{default:e}}function o(){return(o=Object.assign||function(e){for(var t=1;t0?s.default.Children.map(this.props.children,(e,u)=>{if((0,l.isUndefined)(e)||(0,l.isUndefined)(e.props.filterkey,!0)){if(!(0,l.isUndefined)(e)){const i=s.default.cloneElement(e);t.push(i)}}else{let c=e.props.className,p=e.props.children||[];const{filterkey:f,itemDisplayValueFunc:h,itemSortValueFunc:y}=e.props;(0,l.isTypeArray)(p)||(p=[p]),c=(0,l.isUndefined)(c,!0)?"apply-filter":[c," ","apply-filter"].join("");const _=(0,n.calculateFilterProps)({filteredData:i,filterkey:f,itemDisplayValueFunc:h,itemSortValueFunc:y,sortKey:d,sortType:a});"true"!=e.props.filterAdded?p.push(s.default.createElement(r.default,o({},e.props,_,{key:"list_"+u,filterRows:this._filterRows,filterMultipleRows:this._filterMulipleRows,resetRows:this._resetRows,sortRows:this._sortRows}))):p[p.length-1]=s.default.createElement(r.default,o({},e.props,_,{key:"list_"+u,filterRows:this._filterRows,filterMultipleRows:this._filterMulipleRows,resetRows:this._resetRows,sortRows:this._sortRows}));const m={className:c,filteradded:"true"},b=s.default.cloneElement(e,m,[...p]);t.push(b)}}):console.error("TableFilter Error: Should contain one or more children"),(0,l.isUndefined)(this.props.rowComponent))u=s.default.createElement("tr",{className:[this.props.rowClass?this.props.rowClass+" ":"","table-filter-row"].join("")},t);else{const e=this.props.rowComponent,i={className:[this.props.rowClass?this.props.rowClass+" ":"","table-filter-row"].join("")};u=s.default.cloneElement(e,i,[...t])}return u};t.default=d},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=f(i(0)),r=f(i(9)),l=f(i(12)),n=f(i(13)),a=f(i(14)),o=f(i(18)),d=f(i(19)),u=f(i(1)),c=i(3),p=i(2);function f(e){return e&&e.__esModule?e:{default:e}}function h(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}class y extends s.default.Component{constructor(e){super(e),h(this,"_handleOutsideClick",e=>{this.filterIconNode.contains(e.target)||this._hideFilter()}),h(this,"_filterIconClicked",e=>{!this.state.showFilter?this._displayFilter():this._hideFilter()}),h(this,"_displayFilter",()=>{a.default.sub("click",this._handleOutsideClick),this.setState({showFilter:!0})}),h(this,"_hideFilter",()=>{a.default.unsub("click",this._handleOutsideClick),this.setState({showFilter:!1,searchEnabled:!1})}),h(this,"_filterUpdated",e=>{const t=this.props.filterList;if(!(0,p.isUndefined)(t[e])){const i=!t[e].selected;this._filterData(t[e].key,!i)}}),h(this,"_selectAllClicked",()=>{const e=!this.props.selectAllFilters;if(this.state.searchEnabled)return;const t=this.props.filterList.filter(t=>e?t.visible&&!t.selected:t.visible&&t.selected).map(e=>e.key);this._resetData(t,e)}),h(this,"_filterData",(e,t=!0)=>{this.props.filterRows(e,this.props.filterkey,t,this.props.itemDisplayValueFunc)}),h(this,"_resetData",(e=[],t=!0)=>{this.props.resetRows(e,this.props.filterkey,t,this.props.itemDisplayValueFunc)}),h(this,"_sortClicked",()=>{const e=this.props.sortType;let t;t=(0,p.isUndefined)(e)||e===c.DSC_VALUE?c.ASC_VALUE:c.DSC_VALUE,this.props.sortRows(t,{itemSortValueFunc:this.props.itemSortValueFunc,caseSensitive:"true"===this.props.casesensitive,key:this.props.filterkey})}),h(this,"_searchChanged",e=>{const t=this.props.filterkey;this.searchValue=e;const i=this.appliedSearchFilters;if((0,p.isUndefined)(e,!0))this.setState({searchEnabled:!1}),this.appliedSearchFilters=[],this.props.filterMultipleRows([],i,this.props.itemDisplayValueFunc);else{this.setState({searchEnabled:!0}),e=e.toLowerCase();const s=this.props.filterList.filter(t=>!!(t.key.toString().toLowerCase().indexOf(e)<0&&t.visible)).map(e=>({key:t,value:e.key}));this.appliedSearchFilters=s,this.props.filterMultipleRows(s,i,this.props.itemDisplayValueFunc)}}),this.appliedSearchFilters=void 0,this.searchValue=void 0,this.state={showFilter:!1,searchEnabled:!1}}componentWillUnmount(){a.default.unsub("click",this._handleOutsideClick)}render(){const e=this.state.showFilter,t="false"!==this.props.showsearch,i=[];let a;if(this.props.filterList.length>1){if(e){const e=t?s.default.createElement(n.default,{searchChanged:this._searchChanged}):null;this.props.filterList.map((e,t)=>{if(e.visible){if(this.state.searchEnabled){return e.key.toString().toLowerCase().indexOf(this.searchValue.toLowerCase())>=0?i.push(s.default.createElement(r.default,{key:"item_"+t,filterClicked:this._filterUpdated,index:t,label:e.display,selected:e.selected})):null}i.push(s.default.createElement(r.default,{key:"item_"+t,filterClicked:this._filterUpdated,index:t,label:e.display,selected:e.selected}))}});const o=["true"===this.props.alignleft?"align-left ":"","filter-list"].join("");a=s.default.createElement("div",{className:o},e,s.default.createElement(d.default,{sort:this._sortClicked,sortType:this.props.sortType}),s.default.createElement(l.default,{filterClicked:this._selectAllClicked,selected:this.props.selectAllFilters}),i)}const u=!this.props.selectAllFilters||e;return s.default.createElement("div",{className:"table-filter-parent",ref:e=>{this.filterIconNode=e}},s.default.createElement(o.default,{iconClicked:this._filterIconClicked,selected:u}),a)}return s.default.createElement("div",{style:{display:"none"}})}}y.propTypes={filterRows:u.default.func.isRequired,resetRows:u.default.func.isRequired,sortRows:u.default.func.isRequired,sortType:u.default.string,filterkey:u.default.string.isRequired,itemDisplayValueFunc:u.default.func,itemSortValueFunc:u.default.func,casesensitive:u.default.string,filterMultipleRows:u.default.func.isRequired,showsearch:u.default.string,alignleft:u.default.string,filterList:u.default.array,selectAllFilters:u.default.bool};var _=y;t.default=_},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=l(i(0)),r=l(i(1));function l(e){return e&&e.__esModule?e:{default:e}}class n extends s.default.Component{constructor(e){var t,i,s;super(e),s=()=>{this.props.filterClicked(this.props.index)},(i="_checkBoxClicked")in(t=this)?Object.defineProperty(t,i,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[i]=s}render(){const e=[this.props.selected?"selected ":"","filter-check-box"].join("");return s.default.createElement("div",{className:"filter-list-item ripple",onClick:this._checkBoxClicked},s.default.createElement("div",{className:e}),s.default.createElement("div",{className:"filter-label"},this.props.label))}}n.propTypes={filterClicked:r.default.func.isRequired,index:r.default.number.isRequired,label:r.default.any.isRequired,selected:r.default.bool.isRequired};var a=n;t.default=a},function(e,t,i){"use strict";var s=i(11);function r(){}function l(){}l.resetWarningCache=r,e.exports=function(){function e(e,t,i,r,l,n){if(n!==s){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var i={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:l,resetWarningCache:r};return i.PropTypes=i,i}},function(e,t,i){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=l(i(0)),r=l(i(1));function l(e){return e&&e.__esModule?e:{default:e}}class n extends s.default.Component{constructor(e){var t,i,s;super(e),s=()=>{this.props.filterClicked()},(i="_selectAllClicked")in(t=this)?Object.defineProperty(t,i,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[i]=s}render(){const e=[this.props.selected?"selected ":"","filter-check-box"].join("");return s.default.createElement("div",{className:"filter-list-item",onClick:this._selectAllClicked},s.default.createElement("div",{className:e}),s.default.createElement("div",{className:"filter-label select-all-label"},"Select All"))}}n.propTypes={filterClicked:r.default.func.isRequired,selected:r.default.bool.isRequired};var a=n;t.default=a},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=l(i(0)),r=l(i(1));function l(e){return e&&e.__esModule?e:{default:e}}function n(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}class a extends s.default.Component{constructor(e){super(e),n(this,"_searchInputChanged",e=>{const t=e.target.value;this._callSearchChanged(t)}),n(this,"_callSearchChanged",e=>{this.props.searchChanged&&this.props.searchChanged(e)})}render(){return s.default.createElement("div",{className:"search-parent filter-list-item"},s.default.createElement("input",{className:"search-input",type:"text",placeholder:"search",onChange:this._searchInputChanged}))}}a.propTypes={searchChanged:r.default.func.isRequired};var o=a;t.default=o},function(e,t,i){"use strict";var s;Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=((s=i(15))&&s.__esModule?s:{default:s}).default;t.default=r},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=l(i(16)),r=l(i(17));function l(e){return e&&e.__esModule?e:{default:e}}var n=new class{constructor(){this._targets=new Map}_find(e,t=!0){const i=(0,r.default)(e);if(this._targets.has(i))return this._targets.get(i);if(!t)return;const l=new s.default(i);return this._targets.set(i,l),l}_remove(e){const t=(0,r.default)(e);this._targets.delete(t)}sub(e,t,i={}){const{target:s=document,pool:r="default"}=i;this._find(s).sub(e,t,r)}unsub(e,t,i={}){const{target:s=document,pool:r="default"}=i,l=this._find(s,!1);l&&(l.unsub(e,t,r),l.empty()&&this._remove(s))}};t.default=n},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=i(2);t.default=class{constructor(e){this.target=e,this._handlers={},this._pools={}}_emit(e){return e=>{Object.keys(this._pools).forEach(t=>{const i=this._pools[t];i&&i.forEach(t=>t(e))})}}_normalize(e){return(0,s.isTypeArray)(e)?e:[e]}_listen(e){if(this._handlers.hasOwnProperty(e))return;const t=this._emit(e);this.target.addEventListener(e,t),this._handlers[e]=t}_unlisten(e){if(this._pools[e])return;const t=this._handlers[e];this.target.removeEventListener(e,t),delete this._handlers[e]}empty(){return!(this._handlers&&Object.keys(this._handlers).length>0)}sub(e,t){const i=this._normalize(t),r=this._pools[""+e]||[],l=(0,s.uniq)([...r,...i]);this._listen(e),this._pools[""+e]=l}unsub(e,t){const i=this._normalize(t),r=this._pools[""+e]||[],l=(0,s.without)(r,i);l.length>0?this._pools[""+e]=l:(this._pools[""+e]=void 0,this._unlisten(e))}}},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=e=>"document"===e?document:"window"===e?window:e||document;t.default=s},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=l(i(0)),r=l(i(1));function l(e){return e&&e.__esModule?e:{default:e}}class n extends s.default.Component{constructor(e){var t,i,s;super(e),s=()=>{this.props.iconClicked&&this.props.iconClicked()},(i="_iconClicked")in(t=this)?Object.defineProperty(t,i,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[i]=s}render(){const e=[this.props.selected?"selected ":"","table-filter-icon"].join("");return s.default.createElement("div",{onClick:this._iconClicked,className:e})}}n.propTypes={iconClicked:r.default.func.isRequired,selected:r.default.bool};var a=n;t.default=a},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var s=n(i(0)),r=i(2),l=n(i(1));function n(e){return e&&e.__esModule?e:{default:e}}class a extends s.default.Component{constructor(e){var t,i,s;super(e),s=()=>{this.props.sort()},(i="_sortClicked")in(t=this)?Object.defineProperty(t,i,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[i]=s}render(){const e=(0,r.isUndefined)(this.props.sortType)?"":" "+this.props.sortType;return s.default.createElement("div",{className:["sort-parent clear-fix",e].join(""),onClick:this._sortClicked},s.default.createElement("div",{className:"dsc table-filter-arrow"}),s.default.createElement("div",{className:"asc table-filter-arrow"}))}}a.propTypes={sort:l.default.func.isRequired,sortType:l.default.string};var o=a;t.default=o},function(e,t){}])})); -------------------------------------------------------------------------------- /lib/styles.css: -------------------------------------------------------------------------------- 1 | .table-filter-row { 2 | position: relative; } 3 | 4 | .filter-list { 5 | position: absolute; 6 | max-height: 250px; 7 | overflow: auto; 8 | max-width: 220px; 9 | min-width: 170px; 10 | left: 0; 11 | top: 100%; 12 | border: 2px solid #8E9193; } 13 | .filter-list.align-left { 14 | left: auto; 15 | right: 0; } 16 | 17 | .filter-list-item { 18 | position: relative; 19 | padding-left: 30px; 20 | height: 35px; 21 | line-height: 35px; 22 | font-size: 14px; 23 | text-align: left; } 24 | .filter-list-item:nth-child(odd) { 25 | background-color: #F7F6F6; } 26 | .filter-list-item:nth-child(even) { 27 | background-color: white; } 28 | 29 | .filter-check-box { 30 | position: absolute; 31 | width: 14px; 32 | height: 14px; 33 | left: 8px; 34 | top: 50%; 35 | margin-top: -7px; 36 | box-sizing: border-box; 37 | overflow: hidden; 38 | border-radius: 2px; 39 | border: 1px solid #8F8F8F; } 40 | .filter-check-box.selected { 41 | border-color: black; 42 | background-color: black; } 43 | .filter-check-box.selected:after { 44 | content: ''; 45 | position: absolute; 46 | width: 8px; 47 | height: 5px; 48 | box-sizing: border-box; 49 | border-left: 2px solid white; 50 | border-bottom: 2px solid white; 51 | top: 50%; 52 | margin-top: -4px; 53 | left: 2px; 54 | -webkit-transform: rotate(-45deg); 55 | -moz-transform: rotate(-45deg); 56 | -ms-tranfsorm: rotate(-45deg); 57 | -o-transform: rotate(-45deg); 58 | transform: rotate(-45deg); } 59 | 60 | .filter-label { 61 | white-space: nowrap; 62 | overflow: hidden; 63 | text-overflow: ellipsis; 64 | position: relative; 65 | font-weight: normal; } 66 | .filter-label.select-all-label { 67 | font-weight: 600; } 68 | 69 | .apply-filter { 70 | position: relative; 71 | padding-right: 40px; } 72 | 73 | .table-filter-parent { 74 | position: absolute; 75 | right: 3px; 76 | top: 50%; 77 | margin-top: -8px; 78 | z-index: 10; } 79 | 80 | .table-filter-icon { 81 | position: relative; 82 | border-top: 8px solid gray; 83 | box-sizing: border-box; 84 | border-right: 6px solid transparent; 85 | border-left: 6px solid transparent; 86 | width: 0px; 87 | height: 0px; 88 | box-shadow: inset 0 4px gray; 89 | padding: 2px; } 90 | .table-filter-icon.selected { 91 | border-top-color: black; 92 | box-shadow: inset 0 4px black; } 93 | 94 | .ripple { 95 | position: relative; 96 | overflow: hidden; 97 | -webkit-transform: translate3d(0, 0, 0); 98 | -moz-transform: translate3d(0, 0, 0); 99 | -ms-tranfsorm: translate3d(0, 0, 0); 100 | -o-transform: translate3d(0, 0, 0); 101 | transform: translate3d(0, 0, 0); } 102 | 103 | .ripple:after { 104 | content: ""; 105 | display: block; 106 | position: absolute; 107 | width: 100%; 108 | height: 100%; 109 | top: 0; 110 | left: 0; 111 | pointer-events: none; 112 | background-image: radial-gradient(circle, #000 10%, transparent 10.01%); 113 | background-repeat: no-repeat; 114 | background-position: 50%; 115 | -webkit-transform: scale(10, 10); 116 | -moz-transform: scale(10, 10); 117 | -ms-tranfsorm: scale(10, 10); 118 | -o-transform: scale(10, 10); 119 | transform: scale(10, 10); 120 | opacity: 0; 121 | -webkit-transition: transform 0.5s, opacity 1s; 122 | -moz-transition: transform 0.5s, opacity 1s; 123 | -ms-transition: transform 0.5s, opacity 1s; 124 | -o-transition: transform 0.5s, opacity 1s; 125 | transition: transform 0.5s, opacity 1s; } 126 | 127 | .ripple:active:after { 128 | -webkit-transform: scale(0, 0); 129 | -moz-transform: scale(0, 0); 130 | -ms-tranfsorm: scale(0, 0); 131 | -o-transform: scale(0, 0); 132 | transform: scale(0, 0); 133 | opacity: .2; 134 | -webkit-transition: 0s; 135 | -moz-transition: 0s; 136 | -ms-transition: 0s; 137 | -o-transition: 0s; 138 | transition: 0s; } 139 | 140 | .sort-parent { 141 | position: absolute; 142 | background: #F0EEEE; 143 | z-index: 1; 144 | right: 6px; 145 | top: 6px; 146 | border-radius: 4px; 147 | border: 1px solid #E1DDDD; 148 | text-align: center; 149 | padding: 5px 15px; 150 | cursor: pointer; } 151 | .sort-parent.asc .table-filter-arrow.asc { 152 | background: black; } 153 | .sort-parent.asc .table-filter-arrow.asc:after { 154 | border-top-color: black; } 155 | .sort-parent.dsc .table-filter-arrow.dsc { 156 | background: black; } 157 | .sort-parent.dsc .table-filter-arrow.dsc:after { 158 | border-bottom-color: black; } 159 | 160 | .clear-fix:after { 161 | content: ""; 162 | display: table; 163 | clear: both; } 164 | 165 | .table-filter-arrow { 166 | position: relative; 167 | float: left; 168 | width: 2px; 169 | height: 12px; 170 | background: gray; } 171 | .table-filter-arrow.asc { 172 | margin-left: 7px; } 173 | .table-filter-arrow.asc:after { 174 | content: ""; 175 | position: absolute; 176 | border-top: 5px solid gray; 177 | bottom: -1px; 178 | border-left: 5px solid transparent; 179 | border-right: 5px solid transparent; 180 | left: -4px; } 181 | .table-filter-arrow.dsc:after { 182 | content: ""; 183 | position: absolute; 184 | border-bottom: 5px solid gray; 185 | top: -1px; 186 | border-left: 5px solid transparent; 187 | border-right: 5px solid transparent; 188 | left: -4px; } 189 | 190 | .search-parent { 191 | position: relative; 192 | width: 100%; 193 | box-sizing: border-box; 194 | padding-left: 8px; 195 | padding-right: 60px; } 196 | .search-parent .search-input { 197 | position: relative; 198 | width: 100%; 199 | height: 24px; 200 | margin: 0; 201 | padding-left: 5px; 202 | box-sizing: border-box; 203 | font-size: 14px; } 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-table-filter", 3 | "version": "2.0.2", 4 | "description": "", 5 | "main": "lib/bundle.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "./node_modules/.bin/webpack", 9 | "watch": "./node_modules/.bin/webpack --watch", 10 | "start:dev": "./node_modules/.bin/webpack-dev-server --content-base ./docs --inline --hot", 11 | "build-example": "cd examples; ../node_modules/.bin/webpack", 12 | "run-example": "cd examples; ../node_modules/.bin/webpack-dev-server --content-base ../docs --inline --hot --config webpack.config.js", 13 | "release": "npm run build; npm publish", 14 | "pretest": "./node_modules/.bin/eslint ./src", 15 | "bundleanalyze": "analyze=true ./node_modules/.bin/webpack" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/cheekujha/react-table-filter.git" 20 | }, 21 | "author": "cheekujha@gmail.com", 22 | "license": "ISC", 23 | "dependencies": { 24 | "prop-types": "^15.6.0" 25 | }, 26 | "peerDependencies": { 27 | "react": "*", 28 | "react-dom": "*" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.2.2", 32 | "@babel/plugin-proposal-class-properties": "^7.5.5", 33 | "@babel/preset-env": "^7.3.1", 34 | "@babel/preset-react": "^7.0.0", 35 | "babel-eslint": "^10.0.1", 36 | "babel-loader": "^8.0.5", 37 | "babel-plugin-transform-object-assign": "^6.22.0", 38 | "chai": "^4.2.0", 39 | "css-loader": "^3.2.0", 40 | "enzyme": "^3.9.0", 41 | "enzyme-adapter-react-16": "^1.12.1", 42 | "eslint": "^5.12.1", 43 | "eslint-config-google": "^0.11.0", 44 | "eslint-plugin-babel": "^5.3.0", 45 | "eslint-plugin-react": "^7.12.4", 46 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 47 | "html-webpack-plugin": "^3.2.0", 48 | "ignore-styles": "^5.0.1", 49 | "jsdom": "^14.0.0", 50 | "mocha": "^6.2.0", 51 | "node-sass": "^4.14.1", 52 | "sass-loader": "^6.0.6", 53 | "style-loader": "^0.19.0", 54 | "terser-webpack-plugin": "^1.2.1", 55 | "webpack": "^4.29.0", 56 | "webpack-bundle-analyzer": "^3.4.1", 57 | "webpack-cli": "^3.2.1", 58 | "webpack-dev-server": "^3.11.0" 59 | }, 60 | "keywords": [ 61 | "react", 62 | "reactjs", 63 | "react-table", 64 | "react-table-filter", 65 | "tablefilter", 66 | "react-component", 67 | "react-filter-table" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /src/css/tableFilter.scss: -------------------------------------------------------------------------------- 1 | $list-item-odd-color: #F7F6F6; 2 | $list-item-even-color: white; 3 | $checkbox-border-color: #8F8F8F; 4 | $checkbox-selected-border-color: black; 5 | $checkbox-selected-background-color: black; 6 | $list-border-color: #8E9193; 7 | 8 | @mixin transform($value) { 9 | -webkit-transform: #{$value}; 10 | -moz-transform: #{$value}; 11 | -ms-tranfsorm: #{$value}; 12 | -o-transform: #{$value}; 13 | transform: #{$value}; 14 | } 15 | @mixin transition($value...) { 16 | -webkit-transition: #{$value}; 17 | -moz-transition: #{$value}; 18 | -ms-transition: #{$value}; 19 | -o-transition: #{$value}; 20 | transition: #{$value}; 21 | } 22 | 23 | .table-filter-row{ 24 | position: relative; 25 | } 26 | .filter-list{ 27 | position: absolute; 28 | max-height: 250px; 29 | overflow: auto; 30 | max-width: 220px; 31 | min-width: 170px; 32 | left: 0; 33 | top: 100%; 34 | border: 2px solid $list-border-color; 35 | &.align-left{ 36 | left: auto; 37 | right: 0; 38 | } 39 | // color: black; 40 | } 41 | 42 | .filter-list-item{ 43 | // color: black; 44 | position: relative; 45 | padding-left: 30px; 46 | height: 35px; 47 | line-height: 35px; 48 | font-size: 14px; 49 | text-align: left; 50 | &:nth-child(odd){ 51 | background-color: $list-item-odd-color; 52 | } 53 | &:nth-child(even){ 54 | background-color: $list-item-even-color; 55 | } 56 | } 57 | 58 | .filter-check-box{ 59 | position: absolute; 60 | width: 14px; 61 | height: 14px; 62 | left: 8px; 63 | top: 50%; 64 | margin-top: -7px; 65 | box-sizing: border-box; 66 | overflow: hidden; 67 | border-radius: 2px; 68 | border: 1px solid $checkbox-border-color; 69 | &.selected{ 70 | border-color: $checkbox-selected-border-color; 71 | background-color: $checkbox-selected-background-color; 72 | &:after{ 73 | content: ''; 74 | position: absolute; 75 | width: 8px; 76 | height: 5px; 77 | box-sizing: border-box; 78 | border-left: 2px solid white; 79 | border-bottom: 2px solid white; 80 | top: 50%; 81 | margin-top: -4px; 82 | left: 2px; 83 | @include transform(rotate(-45deg)); 84 | } 85 | } 86 | } 87 | 88 | .filter-label{ 89 | white-space: nowrap; 90 | overflow: hidden; 91 | text-overflow: ellipsis; 92 | position: relative; 93 | font-weight: normal; 94 | &.select-all-label{ 95 | font-weight: 600; 96 | } 97 | } 98 | 99 | .apply-filter{ 100 | position: relative; 101 | padding-right: 40px; 102 | } 103 | 104 | .table-filter-parent{ 105 | position: absolute; 106 | right: 3px; 107 | top: 50%; 108 | margin-top: -8px; 109 | z-index: 10; 110 | } 111 | 112 | .table-filter-icon{ 113 | position: relative; 114 | border-top: 8px solid gray; 115 | box-sizing: border-box; 116 | border-right: 6px solid transparent; 117 | border-left: 6px solid transparent; 118 | width: 0px; 119 | height: 0px; 120 | box-shadow: inset 0 4px gray; 121 | padding: 2px; 122 | &.selected{ 123 | border-top-color: black; 124 | box-shadow: inset 0 4px black; 125 | } 126 | } 127 | 128 | .ripple { 129 | position: relative; 130 | overflow: hidden; 131 | @include transform(translate3d(0, 0, 0)); 132 | } 133 | 134 | .ripple:after { 135 | content: ""; 136 | display: block; 137 | position: absolute; 138 | width: 100%; 139 | height: 100%; 140 | top: 0; 141 | left: 0; 142 | pointer-events: none; 143 | background-image: radial-gradient(circle, #000 10%, transparent 10.01%); 144 | background-repeat: no-repeat; 145 | background-position: 50%; 146 | @include transform(scale(10, 10)); 147 | opacity: 0; 148 | @include transition(transform .5s, opacity 1s); 149 | } 150 | 151 | .ripple:active:after { 152 | @include transform(scale(0, 0)); 153 | opacity: .2; 154 | @include transition(0s); 155 | } 156 | 157 | .sort-parent{ 158 | position: absolute; 159 | background: #F0EEEE; 160 | z-index: 1; 161 | right: 6px; 162 | top: 6px; 163 | border-radius: 4px; 164 | border: 1px solid #E1DDDD; 165 | text-align: center; 166 | padding: 5px 15px; 167 | cursor: pointer; 168 | &.asc{ 169 | .table-filter-arrow{ 170 | &.asc{ 171 | background: black; 172 | &:after{ 173 | border-top-color: black; 174 | } 175 | } 176 | } 177 | } 178 | &.dsc{ 179 | .table-filter-arrow{ 180 | &.dsc{ 181 | background: black; 182 | &:after{ 183 | border-bottom-color: black; 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | .clear-fix:after{ 191 | content: ""; 192 | display: table; 193 | clear: both; 194 | } 195 | 196 | .table-filter-arrow{ 197 | position: relative; 198 | float: left; 199 | width: 2px; 200 | height: 12px; 201 | background: gray; 202 | &.asc{ 203 | margin-left: 7px; 204 | &:after{ 205 | content: ""; 206 | position: absolute; 207 | border-top: 5px solid gray; 208 | bottom: -1px; 209 | border-left: 5px solid transparent; 210 | border-right: 5px solid transparent; 211 | left: -4px; 212 | } 213 | } 214 | 215 | &.dsc:after{ 216 | content: ""; 217 | position: absolute; 218 | border-bottom: 5px solid gray; 219 | top: -1px; 220 | border-left: 5px solid transparent; 221 | border-right: 5px solid transparent; 222 | left: -4px; 223 | } 224 | } 225 | 226 | .search-parent{ 227 | position: relative; 228 | width: 100%; 229 | box-sizing: border-box; 230 | padding-left: 8px; 231 | padding-right: 60px; 232 | 233 | .search-input{ 234 | position: relative; 235 | width: 100%; 236 | height: 24px; 237 | margin: 0; 238 | padding-left: 5px; 239 | box-sizing: border-box; 240 | font-size: 14px; 241 | } 242 | } -------------------------------------------------------------------------------- /src/filterIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * FilterIcon 6 | * @extends React 7 | */ 8 | class FilterIcon extends React.Component { 9 | /** 10 | * constructor 11 | * @param {Object} props 12 | */ 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | /** 18 | * _iconClicked filter icon clicked 19 | */ 20 | _iconClicked = () => { 21 | this.props.iconClicked && this.props.iconClicked(); 22 | } 23 | 24 | /** 25 | * render 26 | * @return {JSX} 27 | */ 28 | render() { 29 | const className = [this.props.selected ? 'selected ': '', 'table-filter-icon'].join(''); 30 | return (
); 31 | } 32 | } 33 | 34 | 35 | FilterIcon.propTypes = { 36 | iconClicked: PropTypes.func.isRequired, // Function to be called when filter icon is clicked 37 | selected: PropTypes.bool, 38 | }; 39 | 40 | export default FilterIcon; 41 | -------------------------------------------------------------------------------- /src/filterList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FilterListItem from './filterListItem'; 3 | import SelectAllItem from './selectAllItem'; 4 | import SearchBar from './searchBar'; 5 | import EventStack from './lib/eventStack'; 6 | import FilterIcon from './filterIcon'; 7 | import SortIcon from './sortIcon'; 8 | import PropTypes from 'prop-types'; 9 | import { 10 | ASC_VALUE, 11 | DSC_VALUE, 12 | } from './lib/constants'; 13 | import { 14 | isUndefined, 15 | } from './lib/util'; 16 | 17 | /** 18 | * FilterList List of options displayed in the header of the table 19 | * @extends React 20 | */ 21 | class FilterList extends React.Component { 22 | /** 23 | * constructor 24 | * @param {Object} props 25 | */ 26 | constructor(props) { 27 | super(props); 28 | 29 | this.appliedSearchFilters = undefined; 30 | this.searchValue = undefined; 31 | this.state = { 32 | showFilter: false, 33 | searchEnabled: false, 34 | }; 35 | } 36 | 37 | /** 38 | * componentWillUnmount 39 | */ 40 | componentWillUnmount() { 41 | /* Remove body click listener*/ 42 | EventStack.unsub('click', this._handleOutsideClick); 43 | } 44 | 45 | /** 46 | * _handleOutsideClick Function to hide open filter list when click is done anywhere else 47 | * @param {Event} e 48 | */ 49 | _handleOutsideClick = (e) => { 50 | if (!this.filterIconNode.contains(e.target)) { 51 | this._hideFilter(); 52 | } 53 | } 54 | 55 | /** 56 | * _filterIconClicked function handles click on filter icon 57 | * @param {Event} e 58 | */ 59 | _filterIconClicked = (e) => { 60 | const filterState = this.state.showFilter; 61 | const newState = !filterState; 62 | 63 | if (newState) { 64 | this._displayFilter(); 65 | } else { 66 | this._hideFilter(); 67 | } 68 | } 69 | 70 | /** 71 | * _displayFilter function handles display of filter list 72 | */ 73 | _displayFilter = () => { 74 | /* Body Click listener added*/ 75 | EventStack.sub('click', this._handleOutsideClick); 76 | this.setState({ 77 | showFilter: true, 78 | }); 79 | } 80 | 81 | /** 82 | * _hideFilter function hides filter list 83 | */ 84 | _hideFilter = () => { 85 | /* Body Click listener removed*/ 86 | EventStack.unsub('click', this._handleOutsideClick); 87 | this.setState({ 88 | showFilter: false, 89 | searchEnabled: false, 90 | }); 91 | } 92 | 93 | /** 94 | * _filterUpdated method called when a filter list item is clicked 95 | * @param {Number} index Index of the filter clicked 96 | */ 97 | _filterUpdated = (index) => { 98 | const allFilters = this.props.filterList; 99 | if (!isUndefined(allFilters[index])) { 100 | const newFilterState = !allFilters[index]['selected']; 101 | this._filterData(allFilters[index]['key'], !newFilterState); 102 | } 103 | } 104 | 105 | /** 106 | * [=_selectAllClicked method called when a select all item is clicked 107 | */ 108 | _selectAllClicked = () => { 109 | const selectAllState = this.props.selectAllFilters; 110 | const newSelectAllState = !selectAllState; 111 | const searchState = this.state.searchEnabled; 112 | // const searchValue = this.searchValue; 113 | 114 | if (searchState) { 115 | return; 116 | } 117 | 118 | const visibleFiltersValues = this.props.filterList.filter((filterItem) => { 119 | if (newSelectAllState) { 120 | return (filterItem.visible && !filterItem.selected); 121 | } else { 122 | return (filterItem.visible && filterItem.selected); 123 | } 124 | }).map((filterItem) => { 125 | return filterItem.key; 126 | }); 127 | 128 | this._resetData(visibleFiltersValues, newSelectAllState); 129 | } 130 | 131 | /** 132 | * _filterData Method calls parent props filter mehtod when filters are changed 133 | * @param {String} filterValue Filter value 134 | * @param {Boolean} addFilter Add/Remove 135 | */ 136 | _filterData = (filterValue=undefined, addFilter=true) => { 137 | this.props.filterRows(filterValue, this.props.filterkey, addFilter, this.props.itemDisplayValueFunc); 138 | } 139 | 140 | /** 141 | * _resetData Method calls parent props reset mehtod 142 | * @param {Array} filterValues Array of filter values 143 | * @param {Boolean} selectAll Add/Remove 144 | */ 145 | _resetData = (filterValues=[], selectAll=true) => { 146 | this.props.resetRows(filterValues, this.props.filterkey, selectAll, this.props.itemDisplayValueFunc); 147 | } 148 | 149 | /** 150 | * _sortClicked description 151 | */ 152 | _sortClicked = () => { 153 | const currentSortType = this.props.sortType; 154 | let newSortType; 155 | if (isUndefined(currentSortType) || currentSortType === DSC_VALUE) { 156 | newSortType = ASC_VALUE; 157 | } else { 158 | newSortType = DSC_VALUE; 159 | } 160 | 161 | this.props.sortRows(newSortType, { 162 | itemSortValueFunc: this.props.itemSortValueFunc, 163 | caseSensitive: (this.props.casesensitive === 'true') ? true : false, 164 | key: this.props.filterkey, 165 | }); 166 | } 167 | 168 | /** 169 | * _searchChanged Method called when search is triggered 170 | * @param {String} searchValue [Search value] 171 | */ 172 | _searchChanged = (searchValue) => { 173 | const propKey = this.props.filterkey; 174 | this.searchValue = searchValue; 175 | 176 | 177 | const prevAppliedFilters = this.appliedSearchFilters; 178 | if (!isUndefined(searchValue, true)) { 179 | this.setState({ 180 | searchEnabled: true, 181 | }); 182 | 183 | searchValue = searchValue.toLowerCase(); 184 | const filterList = this.props.filterList; 185 | 186 | const filtersToApply = filterList.filter((filterItem) => { 187 | const filterKey = filterItem.key.toString().toLowerCase(); 188 | if (filterKey.indexOf(searchValue) < 0 && filterItem.visible) { 189 | return true; 190 | } 191 | return false; 192 | }).map((filterItem) => { 193 | return { 194 | key: propKey, 195 | value: filterItem.key, 196 | }; 197 | }); 198 | this.appliedSearchFilters = filtersToApply; 199 | this.props.filterMultipleRows(filtersToApply, prevAppliedFilters, this.props.itemDisplayValueFunc); 200 | } else { 201 | this.setState({ 202 | searchEnabled: false, 203 | }); 204 | 205 | this.appliedSearchFilters = []; 206 | this.props.filterMultipleRows([], prevAppliedFilters, this.props.itemDisplayValueFunc); 207 | } 208 | } 209 | 210 | /** 211 | * render 212 | * @return {JSX} 213 | */ 214 | render() { 215 | const filterState = this.state.showFilter; 216 | const showSearch = this.props.showsearch === 'false' ? false : true; 217 | 218 | const filterListItemHtml = []; 219 | let filterListHtml; 220 | 221 | if (this.props.filterList.length > 1) { 222 | if (filterState) { 223 | const searchBarHtml = showSearch ? : null; 224 | 225 | this.props.filterList.map((filterItem, index) => { 226 | if (filterItem.visible) { 227 | if (this.state.searchEnabled) { 228 | const filterKey = filterItem.key.toString().toLowerCase(); 229 | if (filterKey.indexOf(this.searchValue.toLowerCase()) >= 0) { 230 | return filterListItemHtml.push(); 231 | } else { 232 | return null; 233 | } 234 | } else { 235 | filterListItemHtml.push(); 236 | } 237 | } 238 | }); 239 | 240 | 241 | const filterListClass = [(this.props.alignleft === 'true') ? 'align-left ' : '', 'filter-list'].join(''); 242 | 243 | filterListHtml = (
244 | { searchBarHtml } 245 | 246 | 247 | { filterListItemHtml } 248 |
); 249 | } 250 | 251 | const filterActive = !this.props.selectAllFilters || filterState; 252 | return (
{ 253 | this.filterIconNode = node; 254 | } }> 255 | 256 | { filterListHtml } 257 |
); 258 | } else { 259 | return (
); 260 | } 261 | } 262 | } 263 | 264 | FilterList.propTypes = { 265 | filterRows: PropTypes.func.isRequired, 266 | resetRows: PropTypes.func.isRequired, 267 | sortRows: PropTypes.func.isRequired, 268 | sortType: PropTypes.string, 269 | filterkey: PropTypes.string.isRequired, 270 | itemDisplayValueFunc: PropTypes.func, 271 | itemSortValueFunc: PropTypes.func, 272 | casesensitive: PropTypes.string, 273 | filterMultipleRows: PropTypes.func.isRequired, 274 | showsearch: PropTypes.string, 275 | alignleft: PropTypes.string, 276 | filterList: PropTypes.array, 277 | selectAllFilters: PropTypes.bool, 278 | }; 279 | 280 | export default FilterList; 281 | -------------------------------------------------------------------------------- /src/filterListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * FilterListItem 6 | * @extends React 7 | */ 8 | class FilterListItem extends React.Component { 9 | /** 10 | * constructor 11 | * @param {Object} props 12 | */ 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | /** 18 | * _checkBoxClicked function called when list item is clicked 19 | */ 20 | _checkBoxClicked = () => { 21 | this.props.filterClicked(this.props.index); 22 | } 23 | 24 | /** 25 | * render 26 | * @return {JSX} 27 | */ 28 | render() { 29 | const checkBoxClass = [this.props.selected ? 'selected ' : '', 'filter-check-box'].join(''); 30 | return (
31 |
32 |
{this.props.label}
33 |
); 34 | } 35 | } 36 | 37 | 38 | FilterListItem.propTypes = { 39 | filterClicked: PropTypes.func.isRequired, 40 | index: PropTypes.number.isRequired, 41 | label: PropTypes.any.isRequired, 42 | selected: PropTypes.bool.isRequired, 43 | }; 44 | 45 | export default FilterListItem; 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | import Template from './template.js'; 3 | import { 4 | isUndefined, 5 | } from './lib/util'; 6 | import { 7 | filterAction, 8 | filterActions, 9 | filtersReset, 10 | } from './lib/filter'; 11 | import { 12 | sortAction, 13 | } from './lib/sort'; 14 | import PropTypes from 'prop-types'; 15 | 16 | /** 17 | * TableFilter Main Component 18 | * @extends Component 19 | */ 20 | class TableFilter extends Component { 21 | /** 22 | * constructor 23 | * @param {Object} props 24 | */ 25 | constructor(props) { 26 | super(props); 27 | this.currentFilters = this.props.initialFilters || {}; 28 | const rows = this._applyInitialFilters(this.props.rows); 29 | const stateData = this._createData(rows); 30 | 31 | this.state = { 32 | initialData: stateData.initialData, 33 | filteredData: stateData.filteredData, 34 | sortKey: undefined, 35 | }; 36 | } 37 | 38 | /** 39 | * _applyInitialFilters If initial filters are provided we apply the filters to given data to maintain filter state 40 | * @param {Array} rows Data on which filters will be applied 41 | * @return {Array} Data with filter props applied 42 | */ 43 | _applyInitialFilters = (rows=[]) => { 44 | const currentFilters = this.currentFilters; 45 | if ( !isUndefined(currentFilters) && Object.keys(currentFilters).length > 0 ) { 46 | const filterKeys = Object.keys(currentFilters); 47 | let filteredArray; 48 | filterKeys.map((currKey) => { 49 | const filterValues = currentFilters[currKey]; 50 | const filterToApply = filterValues.map((currValue) => { 51 | return { 52 | key: currKey, 53 | value: currValue, 54 | }; 55 | }); 56 | 57 | const result = filterActions(rows, filterToApply, true, this._getValueFunctionForKey(currKey)); 58 | filteredArray = result.filteredArray; 59 | rows = result.dataWithFilter; 60 | }); 61 | 62 | this.props.onFilterUpdate && this.props.onFilterUpdate(filteredArray, currentFilters); 63 | } 64 | 65 | return rows; 66 | } 67 | 68 | /** 69 | * _getValueFunctionForKey Method to get the itemDisplayValueFunc(if any) provided to individual filter list element 70 | * @param {String} filterKey key 71 | * @return {Function} Value function for that filter list 72 | */ 73 | _getValueFunctionForKey = (filterKey) => { 74 | let valueFunc; 75 | this.props.children.map((child, index) => { 76 | if (!isUndefined(child) && !isUndefined(child.props.filterkey, true) && child.props.filterkey === filterKey) { 77 | valueFunc = child.props.itemDisplayValueFunc; 78 | } 79 | }); 80 | 81 | return valueFunc; 82 | } 83 | 84 | // This method to be done with web worker 85 | /** 86 | * _createData The data passed via props is copied to another array(to avoid mutation). // TODO - only one level 87 | * of mutation is prohibited right now. 88 | * @param {Array} rows Data passed through props 89 | * @return {Object} Created Data to be used in Filtering 90 | */ 91 | _createData = (rows = []) => { 92 | const initialData = []; 93 | const filteredData = []; 94 | rows.map((item) => { 95 | initialData.push(Object.assign({}, item)); 96 | filteredData.push(Object.assign({}, item)); 97 | }); 98 | 99 | return { 100 | initialData, 101 | filteredData, 102 | }; 103 | } 104 | 105 | /** 106 | * _filterMulipleRows function handles add/remove of multiple filters. Ex. 107 | * in case of select all or search etc 108 | * @param {Array} [addFilterArray=[]] Filters to add 109 | * @param {Array} [removeFilterArray=[]] Filter to remove 110 | * @param {Function} [valueFunc=undefined] Optional function used to get value for item 111 | */ 112 | _filterMulipleRows = (addFilterArray=[], removeFilterArray=[], valueFunc=undefined) => { 113 | const filteredData = this.state.filteredData; 114 | 115 | if (!isUndefined(addFilterArray)) { 116 | removeFilterArray.map((filterItem) => { 117 | this._updateCurrentFilter(filterItem.value, false, filterItem.key); 118 | }); 119 | 120 | addFilterArray.map((filterItem) => { 121 | this._updateCurrentFilter(filterItem.value, true, filterItem.key); 122 | }); 123 | 124 | let result = filterActions(filteredData, removeFilterArray, false, valueFunc); 125 | 126 | result = filterActions(result.dataWithFilter, addFilterArray, true, valueFunc); 127 | 128 | if (!isUndefined(result)) { 129 | const filteredArray = result.filteredArray; 130 | const dataWithFilter = result.dataWithFilter; 131 | 132 | this.setState({ 133 | filteredData: dataWithFilter, 134 | }); 135 | this.props.onFilterUpdate && this.props.onFilterUpdate(filteredArray, this._getCurrentFilters()); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * _filterRows Function passed as a prop to FilterList Componenet and called in case a filter is applied 142 | * or removed 143 | * @param {String} value Filter value 144 | * @param {String} key Filter key 145 | * @param {Boolean} addFilter Add or remove fitler 146 | * @param {Function} valueFunc(optional) Function passed to calculate the value of an item 147 | */ 148 | _filterRows = (value=undefined, key=undefined, addFilter=true, valueFunc=undefined) => { 149 | const filteredData = this.state.filteredData; 150 | if (!isUndefined(value) && !isUndefined(key)) { 151 | this._updateCurrentFilters([value], addFilter, key); 152 | const result = filterAction(filteredData, {key, value}, addFilter, valueFunc); 153 | if (!isUndefined(result)) { 154 | const filteredArray = result.filteredArray; 155 | const dataWithFilter = result.dataWithFilter; 156 | 157 | this.setState({ 158 | filteredData: dataWithFilter, 159 | }); 160 | this.props.onFilterUpdate && this.props.onFilterUpdate(filteredArray, this._getCurrentFilters()); 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * _updateCurrentFilter Method to update current filter configuration, used to maintain state if component is unmounted 167 | * @param {String} filter Filter value to add/remove 168 | * @param {Boolean} add add/remove filter option 169 | * @param {String} key Filter key 170 | */ 171 | _updateCurrentFilter = (filter, add=true, key=undefined) => { 172 | if (!isUndefined(key, true) && !isUndefined(filter, true)) { 173 | if (isUndefined(this.currentFilters[key])) { 174 | this.currentFilters[key] = []; 175 | } 176 | 177 | if (add) { 178 | if (this.currentFilters[key].indexOf(filter) < 0) { 179 | this.currentFilters[key].push(filter); 180 | } 181 | } else { 182 | if (this.currentFilters[key].indexOf(filter) >= 0) { 183 | const index = this.currentFilters[key].indexOf(filter); 184 | this.currentFilters[key] = [...this.currentFilters[key].slice(0, index), ...this.currentFilters[key].slice(index + 1)]; 185 | } 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * _updateCurrentFilters Mehtod to update current filter configuration with multiple values 192 | * @param {Array} filters Array of filters to add/remove 193 | * @param {Boolean} add add/remove filter option 194 | * @param {String} key Filter key 195 | */ 196 | _updateCurrentFilters = (filters=[], add=true, key) => { 197 | if (!isUndefined(filters) && !isUndefined(key)) { 198 | filters.map((filterItem) => { 199 | this._updateCurrentFilter(filterItem, add, key); 200 | }); 201 | } 202 | } 203 | 204 | /** 205 | * _getCurrentFilters get method for current filter configuration 206 | * @return {Object} current filter configuration 207 | */ 208 | _getCurrentFilters = () => { 209 | return this.currentFilters; 210 | } 211 | 212 | /** 213 | * _resetRows Function called to reset the selected filters. 214 | * @param {Array} filterValues Array of values for which filter is to be removed 215 | * @param {String} key Key of the filter to be removed 216 | * @param {Boolean} selectAll option to select all / remove all 217 | * @param {Function} valueFunc Get value function for filter 218 | */ 219 | _resetRows = (filterValues=[], key=undefined, selectAll=true, valueFunc=undefined) => { 220 | if (!isUndefined(key)) { 221 | const filteredData = this.state.filteredData; 222 | this._updateCurrentFilters(filterValues, !selectAll, key); 223 | const result = filtersReset(filteredData, filterValues, key, selectAll, valueFunc); 224 | if (!isUndefined(result)) { 225 | const filteredArray = result.filteredArray; 226 | const dataWithFilter = result.dataWithFilter; 227 | 228 | this.setState({ 229 | filteredData: dataWithFilter, 230 | }); 231 | this.props.onFilterUpdate && this.props.onFilterUpdate(filteredArray, this._getCurrentFilters()); 232 | } 233 | } 234 | } 235 | 236 | /** 237 | * _sortRows Function to sort the values according to a filter 238 | * @param {String} sortType 239 | * @param {[type]} options.valueFunc (optional) Function to calculate the value of the item 240 | * @param {Boolean} options.caseSensitive Case Sensitive or not 241 | * @param {[String]} options.key Key to sort by 242 | */ 243 | _sortRows = (sortType=undefined, {valueFunc=undefined, caseSensitive=false, key=undefined} = {}) => { 244 | if (!isUndefined(sortType)) { 245 | const filteredData = this.state.filteredData; 246 | const result = sortAction(filteredData, sortType, {valueFunc, caseSensitive, key} ); 247 | 248 | const filteredArray = []; 249 | 250 | this.setState({ 251 | filteredData: result, 252 | sortKey: key, 253 | sortType: sortType, 254 | }); 255 | 256 | result.map((item) => { 257 | if (isUndefined(item.appliedFilters) || Object.keys(item.appliedFilters).length === 0) { 258 | const itemCopy = Object.assign({}, item); 259 | delete itemCopy['appliedFilters']; 260 | filteredArray.push(itemCopy); 261 | } 262 | }); 263 | 264 | this.props.onFilterUpdate && this.props.onFilterUpdate(filteredArray, this._getCurrentFilters()); 265 | } 266 | } 267 | 268 | /** 269 | * reset Function called from parent(main code) to load/reset data of the filters 270 | * @param {Array} rows 271 | * @param {Boolean} resetFilters Option to reset current filters 272 | */ 273 | reset = (rows, resetFilters=true) => { 274 | if (!resetFilters) { 275 | rows = this._applyInitialFilters(rows); 276 | } else { 277 | this.currentFilters = {}; 278 | } 279 | 280 | const stateData = this._createData(rows); 281 | this.setState({ 282 | initialData: stateData.initialData, 283 | filteredData: stateData.filteredData, 284 | }); 285 | } 286 | 287 | /** 288 | * render 289 | * @return {JSX} 290 | */ 291 | render() { 292 | return Template.call(this); 293 | } 294 | } 295 | 296 | TableFilter.propTypes = { 297 | rows: PropTypes.array.isRequired, // Filterable Data 298 | onFilterUpdate: PropTypes.func.isRequired, // Function to be called with updated data when filters are applied or removed 299 | rowClass: PropTypes.string, // Optional class to be attached to row elements 300 | initialFilters: PropTypes.array, // Initial filter configuration if any(provided as a parameter in onFilterUpdate) 301 | rowComponent: PropTypes.func, 302 | children: PropTypes.any, 303 | }; 304 | 305 | export default TableFilter; 306 | -------------------------------------------------------------------------------- /src/lib/constants.js: -------------------------------------------------------------------------------- 1 | export const BLANK_LABEL = '(blank)'; 2 | export const ASC_VALUE = 'asc'; 3 | export const DSC_VALUE = 'dsc'; 4 | 5 | export default { 6 | BLANK_LABEL, 7 | ASC_VALUE, 8 | DSC_VALUE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/debounce.js: -------------------------------------------------------------------------------- 1 | module.exports = (func, wait, immediate) => { 2 | let timeout; let args; let context; let timestamp; let result; 3 | if (null == wait) wait = 100; 4 | 5 | function later() { 6 | const last = Date.now() - timestamp; 7 | 8 | if (last < wait && last >= 0) { 9 | timeout = setTimeout(later, wait - last); 10 | } else { 11 | timeout = null; 12 | if (!immediate) { 13 | result = func.apply(context, args); 14 | context = args = null; 15 | } 16 | } 17 | } 18 | 19 | const debounced = function() { 20 | context = this; 21 | args = arguments; 22 | timestamp = Date.now(); 23 | const callNow = immediate && !timeout; 24 | if (!timeout) timeout = setTimeout(later, wait); 25 | if (callNow) { 26 | result = func.apply(context, args); 27 | context = args = null; 28 | } 29 | 30 | return result; 31 | }; 32 | 33 | debounced.clear = function() { 34 | if (timeout) { 35 | clearTimeout(timeout); 36 | timeout = null; 37 | } 38 | }; 39 | 40 | debounced.flush = function() { 41 | if (timeout) { 42 | result = func.apply(context, args); 43 | context = args = null; 44 | 45 | clearTimeout(timeout); 46 | timeout = null; 47 | } 48 | }; 49 | 50 | return debounced; 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/eventStack/eventStack.js: -------------------------------------------------------------------------------- 1 | import EventTarget from './eventTarget'; 2 | import normalizeTarget from './normalizeTarget'; 3 | 4 | class EventStack { 5 | constructor() { 6 | this._targets = new Map(); 7 | } 8 | 9 | // ------------------------------------ 10 | // Target utils 11 | // ------------------------------------ 12 | 13 | _find(target, autoCreate = true) { 14 | const normalized = normalizeTarget(target); 15 | 16 | if (this._targets.has(normalized)) return this._targets.get(normalized); 17 | if (!autoCreate) return; 18 | 19 | const eventTarget = new EventTarget(normalized); 20 | this._targets.set(normalized, eventTarget); 21 | 22 | return eventTarget; 23 | } 24 | 25 | _remove(target) { 26 | const normalized = normalizeTarget(target); 27 | 28 | this._targets.delete(normalized); 29 | } 30 | 31 | sub(name, handlers, options = {}) { 32 | const {target = document, pool = 'default'} = options; 33 | const eventTarget = this._find(target); 34 | 35 | eventTarget.sub(name, handlers, pool); 36 | } 37 | 38 | unsub(name, handlers, options = {}) { 39 | const {target = document, pool = 'default'} = options; 40 | const eventTarget = this._find(target, false); 41 | 42 | if (eventTarget) { 43 | eventTarget.unsub(name, handlers, pool); 44 | if (eventTarget.empty()) this._remove(target); 45 | } 46 | } 47 | } 48 | 49 | const instance = new EventStack(); 50 | 51 | export default instance; 52 | -------------------------------------------------------------------------------- /src/lib/eventStack/eventTarget.js: -------------------------------------------------------------------------------- 1 | import { 2 | uniq, 3 | without, 4 | isTypeArray, 5 | } from '../util'; 6 | 7 | export default class EventTarget { 8 | constructor(target) { 9 | this.target = target; 10 | this._handlers = {}; 11 | this._pools = {}; 12 | } 13 | 14 | // ------------------------------------ 15 | // Utils 16 | // ------------------------------------ 17 | 18 | _emit(name) { 19 | return (event) => { 20 | Object.keys(this._pools).forEach((poolName) => { 21 | const handlers = this._pools[poolName]; 22 | if (!handlers) return; 23 | handlers.forEach((handler) => handler(event)); 24 | return; 25 | }); 26 | }; 27 | } 28 | 29 | _normalize(handlers) { 30 | return isTypeArray(handlers) ? handlers : [handlers]; 31 | } 32 | 33 | // ------------------------------------ 34 | // Listeners handling 35 | // ------------------------------------ 36 | 37 | _listen(name) { 38 | if (this._handlers.hasOwnProperty(name)) return; 39 | const handler = this._emit(name); 40 | 41 | this.target.addEventListener(name, handler); 42 | this._handlers[name] = handler; 43 | } 44 | 45 | _unlisten(name) { 46 | if (this._pools[name]) return; 47 | const handler = this._handlers[name]; 48 | 49 | this.target.removeEventListener(name, handler); 50 | delete this._handlers[name]; 51 | } 52 | 53 | empty() { 54 | return (this._handlers && Object.keys(this._handlers).length > 0) ? false : true; 55 | } 56 | 57 | // ------------------------------------ 58 | // Pub/sub 59 | // ------------------------------------ 60 | 61 | sub(name, handlers) { 62 | const newHandlers = this._normalize(handlers); 63 | const prevHandlers = this._pools[`${name}`] || []; 64 | const events = uniq([ 65 | ...prevHandlers, 66 | ...newHandlers, 67 | ]); 68 | 69 | this._listen(name); 70 | this._pools[`${name}`] = events; 71 | } 72 | 73 | unsub(name, handlers) { 74 | const toRemoveHandlers = this._normalize(handlers); 75 | const prevHandlers = this._pools[`${name}`] || []; 76 | const events = without( 77 | prevHandlers, 78 | toRemoveHandlers, 79 | ); 80 | 81 | if (events.length > 0) { 82 | this._pools[`${name}`] = events; 83 | return; 84 | } 85 | this._pools[`${name}`] = undefined; 86 | this._unlisten(name); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/lib/eventStack/index.js: -------------------------------------------------------------------------------- 1 | import EventStack from './eventStack'; 2 | 3 | export default EventStack; 4 | -------------------------------------------------------------------------------- /src/lib/eventStack/normalizeTarget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalizes `target` for EventStack, because `target` can be passed as `boolean` or `string`. 3 | * 4 | * @param {boolean|string|HTMLElement|Window} target Value for normalization. 5 | * @return {HTMLElement|Window} A DOM node. 6 | */ 7 | const normalizeTarget = (target) => { 8 | if (target === 'document') return document; 9 | if (target === 'window') return window; 10 | return target || document; 11 | }; 12 | 13 | export default normalizeTarget; 14 | -------------------------------------------------------------------------------- /src/lib/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | isUndefined, 5 | isTypeString, 6 | getValForKey, 7 | } from './util'; 8 | 9 | import { 10 | sortAction, 11 | } from './sort'; 12 | import { 13 | BLANK_LABEL, 14 | ASC_VALUE, 15 | } from './constants'; 16 | 17 | export const filterActions = (inputArray=[], filterArray=[], addFilter=true, valueFunc=undefined) => { 18 | const filteredArray = []; 19 | const dataWithFilter = inputArray.map((item) => { 20 | const itemCopy = Object.assign({}, item); 21 | 22 | let i; let l; 23 | 24 | if (isUndefined(itemCopy.appliedFilters)) { 25 | itemCopy.appliedFilters = {}; 26 | } 27 | 28 | for (i=0, l=filterArray.length; i { 92 | const key = filter.key; 93 | let value = filter.value; 94 | 95 | if (isUndefined(value)) { 96 | value = ''; 97 | } 98 | if (!isUndefined(key)) { 99 | const filteredArray = []; 100 | const dataWithFilter = inputArray.map((item) => { 101 | const itemCopy = Object.assign({}, item); 102 | let itemValue = getValForKey(item, key); 103 | 104 | if (!isUndefined(valueFunc)) { 105 | itemValue = valueFunc(itemValue); 106 | } 107 | 108 | if (isUndefined(itemValue)) { 109 | itemValue = ''; 110 | } 111 | 112 | if (isUndefined(itemCopy.appliedFilters)) { 113 | itemCopy.appliedFilters = {}; 114 | } 115 | 116 | if (isTypeString(itemValue)) { 117 | itemValue = itemValue.trim(); 118 | } 119 | 120 | if (addFilter) { 121 | if (itemValue === value) { 122 | if (!itemCopy.appliedFilters[key]) { 123 | itemCopy.appliedFilters[key] = 0; 124 | } 125 | itemCopy.appliedFilters[key] += 1; 126 | } 127 | } else { 128 | if (itemValue === value) { 129 | itemCopy.appliedFilters[key] -= 1; 130 | if (itemCopy.appliedFilters[key] === 0) { 131 | delete itemCopy.appliedFilters[key]; 132 | } 133 | } 134 | } 135 | 136 | if (Object.keys(itemCopy.appliedFilters).length === 0) { 137 | delete itemCopy['appliedFilters']; 138 | filteredArray.push(Object.assign({}, itemCopy)); 139 | } 140 | 141 | return itemCopy; 142 | }); 143 | 144 | return { 145 | filteredArray, 146 | dataWithFilter, 147 | }; 148 | } 149 | 150 | return; 151 | }; 152 | 153 | /** 154 | * [Function to reset certain values for a filter] 155 | * @param {Array} inputArray [Array to be filtered] 156 | * @param {Array} values [Filter values to reset] 157 | * @param {[type]} key [Filter key] 158 | * @param {Boolean} selectAll 159 | * @param {Function} valueFunc [Function to calculate value of the property(optional)] 160 | * @return {[type]} 161 | */ 162 | export const filtersReset = (inputArray=[], values=[], key=undefined, selectAll=true, valueFunc=undefined) => { 163 | const filteredArray = []; 164 | const dataWithFilter = inputArray.map((item) => { 165 | const itemCopy = Object.assign({}, item); 166 | 167 | if (isUndefined(itemCopy.appliedFilters)) { 168 | itemCopy.appliedFilters = {}; 169 | } 170 | 171 | let itemValue = getValForKey(itemCopy, key); 172 | 173 | if (!isUndefined(valueFunc)) { 174 | itemValue = valueFunc(itemValue); 175 | } 176 | 177 | if (isUndefined(itemValue)) { 178 | itemValue = ''; 179 | } 180 | 181 | if (isTypeString(itemValue)) { 182 | itemValue = itemValue.trim(); 183 | } 184 | 185 | if (values.indexOf(itemValue) >= 0) { 186 | if (selectAll) { 187 | delete itemCopy.appliedFilters[key]; 188 | } else { 189 | if (!itemCopy.appliedFilters[key]) { 190 | itemCopy.appliedFilters[key] = 0; 191 | } 192 | itemCopy.appliedFilters[key]++; 193 | } 194 | } 195 | 196 | if (Object.keys(itemCopy.appliedFilters).length === 0) { 197 | delete itemCopy['appliedFilters']; 198 | filteredArray.push(Object.assign({}, itemCopy)); 199 | } 200 | 201 | return itemCopy; 202 | }); 203 | 204 | return { 205 | filteredArray, 206 | dataWithFilter, 207 | }; 208 | }; 209 | 210 | /** 211 | * createFiltersFromItems calculate current filter's list items and state 212 | * @param {Array} dataArray Input Item list 213 | * @param {String} filterkey 214 | * @param {Function} itemDisplayValueFunc 215 | * @param {Function} itemSortValueFunc 216 | * @return {Object} 217 | */ 218 | export const createFiltersFromItems = (dataArray, filterkey, itemDisplayValueFunc, itemSortValueFunc) => { 219 | const filteredData = dataArray ? [...dataArray] : []; 220 | const usedKeys = []; 221 | let filterList = []; 222 | 223 | let selectState = true; 224 | 225 | filteredData.map((item) => { 226 | let itemKey = getValForKey(item, filterkey); 227 | let orinigalValue = itemKey; 228 | 229 | if (!isUndefined(itemDisplayValueFunc)) { 230 | itemKey = itemDisplayValueFunc(itemKey); 231 | } 232 | 233 | const appliedFilters = item.appliedFilters || {}; 234 | let displayName = itemKey; 235 | 236 | if (isUndefined(itemKey)) { 237 | displayName = BLANK_LABEL; 238 | itemKey = ''; 239 | orinigalValue = displayName; 240 | } else if ((isTypeString(itemKey))) { 241 | itemKey = itemKey.trim(); 242 | if (itemKey.length === 0) { 243 | displayName = BLANK_LABEL; 244 | orinigalValue = displayName; 245 | } 246 | } 247 | 248 | if (usedKeys.indexOf(itemKey) === -1) { 249 | if (!isUndefined(appliedFilters) && Object.keys(appliedFilters).length > 0) { 250 | if (Object.keys(appliedFilters).length === 1 && Object.keys(appliedFilters)[0] === filterkey) { 251 | selectState = false; 252 | filterList.push({ 253 | 'key': itemKey, 254 | 'display': displayName, 255 | 'selected': false, 256 | 'visible': true, 257 | 'orinigalValue': orinigalValue, 258 | }); 259 | } else { 260 | filterList.push({ 261 | 'key': itemKey, 262 | 'display': displayName, 263 | 'selected': true, 264 | 'visible': false, 265 | 'orinigalValue': orinigalValue, 266 | }); 267 | } 268 | } else { 269 | filterList.push({ 270 | 'key': itemKey, 271 | 'display': displayName, 272 | 'selected': true, 273 | 'visible': true, 274 | 'orinigalValue': orinigalValue, 275 | }); 276 | } 277 | 278 | usedKeys.push(itemKey); 279 | } else { 280 | const filterIndex = usedKeys.indexOf(itemKey); 281 | let filterItem = filterList[filterIndex]; 282 | if (Object.keys(appliedFilters).length === 0) { 283 | if (!filterItem.selected || !filterItem.visible) { 284 | filterItem = Object.assign({}, filterItem, {'selected': true, 'visible': true}); 285 | filterList[filterIndex] = filterItem; 286 | } 287 | } 288 | 289 | if (Object.keys(appliedFilters).length === 1 && Object.keys(appliedFilters)[0] === filterkey) { 290 | selectState = false; 291 | filterItem = Object.assign({}, filterItem, {'selected': false, 'visible': true}); 292 | filterList[filterIndex] = filterItem; 293 | } 294 | } 295 | }); 296 | 297 | filterList = sortAction(filterList, ASC_VALUE, { 298 | valueFunc: itemSortValueFunc, 299 | key: 'orinigalValue', 300 | }); 301 | 302 | return { 303 | filterList, 304 | selectState, 305 | }; 306 | }; 307 | 308 | /** 309 | * calculateFilterProps calculate single filterList props from input data and other parameters 310 | * @param {Array} filteredData input data 311 | * @param {String} filterkey object key for the current filter 312 | * @param {Function} itemDisplayValueFunc 313 | * @param {Function} itemSortValueFunc 314 | * @param {String} sortKey current sort key if any 315 | * @param {String} sortType 316 | * @return {Object} 317 | */ 318 | export const calculateFilterProps = ({ 319 | filteredData, 320 | filterkey, 321 | itemDisplayValueFunc, 322 | itemSortValueFunc, 323 | sortKey, 324 | sortType, 325 | }) => { 326 | const {filterList, selectState} = createFiltersFromItems(filteredData, filterkey, itemDisplayValueFunc, itemSortValueFunc); 327 | const sortTypeState = (!isUndefined(sortKey) && (sortKey === filterkey) ) ? sortType : undefined; 328 | 329 | return { 330 | filterList: filterList, 331 | selectAllFilters: selectState, 332 | sortType: sortTypeState, 333 | }; 334 | }; 335 | -------------------------------------------------------------------------------- /src/lib/sort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | isUndefined, 5 | isTypeString, 6 | getValForKey, 7 | } from './util'; 8 | 9 | /** 10 | * Function to sort an array (asc or dsc) w.r.t to an filter 11 | * @param {Array} inputArray Array to be sorted 12 | * @param {String} type asc or dsc 13 | * @param {Function} options.valueFunc Function to calculate value of an item(optional) 14 | * @param {Boolean} options.caseSensitive Whether case sensitive or not 15 | * @param {String} options.key Filter Key 16 | * @return {Array} Sorted array 17 | */ 18 | export const sortAction = (inputArray=[], type=undefined, {valueFunc=undefined, caseSensitive=false, key=undefined} ={}) => { 19 | if (!isUndefined(type)) { 20 | const inputCopy = [...inputArray]; 21 | 22 | const sortFunc = (a, b) => { 23 | let aValue; let bValue; 24 | 25 | if (!isUndefined(key)) { 26 | aValue = getValForKey(a, key); 27 | bValue = getValForKey(b, key); 28 | } else { 29 | aValue = a; 30 | bValue = b; 31 | } 32 | 33 | if (!isUndefined(valueFunc)) { 34 | aValue = valueFunc(aValue); 35 | bValue = valueFunc(bValue); 36 | } else { 37 | if (!isNaN(Number(aValue)) && !isNaN(Number(bValue))) { 38 | aValue = Number(aValue); 39 | bValue = Number(bValue); 40 | } 41 | 42 | if (isTypeString(aValue)) { 43 | aValue = aValue.trim(); 44 | if (!caseSensitive) { 45 | aValue = aValue.toUpperCase(); 46 | } 47 | } 48 | 49 | if (isTypeString(bValue)) { 50 | bValue = bValue.trim(); 51 | if (!caseSensitive) { 52 | bValue = bValue.toUpperCase(); 53 | } 54 | } 55 | } 56 | 57 | if (isUndefined(aValue)) { 58 | aValue = ''; 59 | } 60 | 61 | if (isUndefined(bValue)) { 62 | bValue = ''; 63 | } 64 | 65 | let result = 0; 66 | 67 | if (aValue < bValue) { 68 | result = -1; 69 | } else { 70 | result = 1; 71 | } 72 | 73 | if (type === 'asc') { 74 | return result; 75 | } else { 76 | return -(result); 77 | } 78 | }; 79 | 80 | return inputCopy.sort(sortFunc); 81 | } 82 | return inputArray; 83 | }; 84 | 85 | export default sortAction; 86 | -------------------------------------------------------------------------------- /src/lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const isUndefined = (str, emptyStringCheck) => { 4 | if (typeof str === 'undefined' || str === null || str === 'undefined' || str === 'null') { 5 | return true; 6 | } 7 | if (emptyStringCheck && typeof str === 'string' && str.toString().trim().length === 0) { 8 | return true; 9 | } 10 | return false; 11 | }; 12 | 13 | export const isTypeArray = (val) => { 14 | return Object.prototype.toString.call(val) === '[object Array]' ? true : false; 15 | }; 16 | 17 | export const isTypeString = (val) => { 18 | return Object.prototype.toString.call(val) === '[object String]' ? true : false; 19 | }; 20 | 21 | export const getValForKey = (obj, key) => { 22 | if (!isUndefined(key)) { 23 | if (isTypeString(key)) { 24 | const keyArray = key.split('.'); 25 | 26 | if (keyArray.length === 1) { 27 | return obj[key]; 28 | } else { 29 | let finalValue = obj; 30 | let i; let l; 31 | for (i=0, l=keyArray.length; i < l; i=i+1) { 32 | const currKey = keyArray[i]; 33 | const currValue = finalValue[currKey]; 34 | 35 | if (!isUndefined(currValue)) { 36 | finalValue = currValue; 37 | } else { 38 | finalValue = undefined; 39 | break; 40 | } 41 | } 42 | 43 | return finalValue; 44 | } 45 | } else { 46 | return obj[key]; 47 | } 48 | } 49 | return; 50 | }; 51 | 52 | export const uniq = (array) => { 53 | if ((array != null && array.length)) { 54 | const unique = [...new Set(array)]; 55 | return unique; 56 | } 57 | return []; 58 | }; 59 | 60 | export const without = (array, values=[]) => { 61 | const result = []; 62 | 63 | if (!array.length) { 64 | return result; 65 | } 66 | 67 | array.forEach((currItem) => { 68 | if (values.indexOf(currItem) < 0) { 69 | result.push(currItem); 70 | } 71 | }); 72 | 73 | return result; 74 | }; 75 | 76 | export default { 77 | isUndefined, 78 | isTypeArray, 79 | isTypeString, 80 | getValForKey, 81 | uniq, 82 | without, 83 | }; 84 | -------------------------------------------------------------------------------- /src/searchBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * SearchBar search input component 6 | * @extends React 7 | */ 8 | class SearchBar extends React.Component { 9 | /** 10 | * constructor 11 | * @param {Object} props 12 | */ 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | /** 18 | * _searchInputChanged 19 | * @param {Event} e 20 | */ 21 | _searchInputChanged = (e) => { 22 | const newValue = e.target.value; 23 | 24 | this._callSearchChanged(newValue); 25 | } 26 | 27 | /** 28 | * _callSearchChanged function called to update the filters according to search 29 | * @param {String} val 30 | */ 31 | _callSearchChanged = (val) => { 32 | this.props.searchChanged && this.props.searchChanged(val); 33 | } 34 | 35 | /** 36 | * render 37 | * @return {JSX} 38 | */ 39 | render() { 40 | return ( 41 |
42 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | SearchBar.propTypes = { 49 | searchChanged: PropTypes.func.isRequired, 50 | }; 51 | 52 | export default SearchBar; 53 | -------------------------------------------------------------------------------- /src/selectAllItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * SelectAllItem Select all list item 6 | * @extends React 7 | */ 8 | class SelectAllItem extends React.Component { 9 | /** 10 | * constructor 11 | * @param {Object} props 12 | */ 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | /** 18 | * _selectAllClicked 19 | */ 20 | _selectAllClicked = () => { 21 | this.props.filterClicked(); 22 | } 23 | 24 | /** 25 | * render 26 | * @return {JSX} 27 | */ 28 | render() { 29 | const checkBoxClass = [this.props.selected ? 'selected ' : '', 'filter-check-box'].join(''); 30 | return (
31 |
32 |
Select All
33 |
); 34 | } 35 | } 36 | 37 | SelectAllItem.propTypes = { 38 | filterClicked: PropTypes.func.isRequired, 39 | selected: PropTypes.bool.isRequired, 40 | }; 41 | 42 | export default SelectAllItem; 43 | -------------------------------------------------------------------------------- /src/sortIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | isUndefined, 4 | } from './lib/util'; 5 | import PropTypes from 'prop-types'; 6 | 7 | class SortIcon extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | _sortClicked = () => { 13 | this.props.sort(); 14 | } 15 | 16 | render() { 17 | const sortClass = !isUndefined(this.props.sortType) ? (' ' + this.props.sortType) : ''; 18 | return (
19 |
20 |
21 |
); 22 | } 23 | } 24 | 25 | SortIcon.propTypes = { 26 | sort: PropTypes.func.isRequired, 27 | sortType: PropTypes.string, 28 | }; 29 | 30 | export default SortIcon; 31 | -------------------------------------------------------------------------------- /src/template.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FilterList from './filterList'; 3 | import { 4 | isUndefined, 5 | isTypeArray, 6 | } from './lib/util'; 7 | import styles from './css/tableFilter.scss'; 8 | import { 9 | calculateFilterProps, 10 | } from './lib/filter'; 11 | 12 | /** 13 | * Main render function of the TableFilter 14 | * @return {JSX} 15 | */ 16 | const render = function() { 17 | const children = this.props.children; 18 | const finalChildren = []; 19 | const { 20 | filteredData, 21 | sortType, 22 | sortKey, 23 | } = this.state; 24 | 25 | if (!isUndefined(children) && children.length > 0) { 26 | React.Children.map(this.props.children, (child, index) => { 27 | if (!isUndefined(child) && !isUndefined(child.props.filterkey, true)) { 28 | let childClass = child.props.className; 29 | let childChildren = child.props.children || []; 30 | const { 31 | filterkey, 32 | itemDisplayValueFunc, 33 | itemSortValueFunc, 34 | } = child.props; 35 | 36 | if (!isTypeArray(childChildren)) { 37 | childChildren = [childChildren]; 38 | } 39 | 40 | if (isUndefined(childClass, true)) { 41 | childClass = 'apply-filter'; 42 | } else { 43 | childClass = [childClass, ' ', 'apply-filter'].join(''); 44 | } 45 | 46 | const filterProps = calculateFilterProps({ 47 | filteredData, 48 | filterkey, 49 | itemDisplayValueFunc, 50 | itemSortValueFunc, 51 | sortKey, 52 | sortType, 53 | }); // filterList, selectAllFilters, sortType 54 | 55 | if (child.props.filterAdded != 'true') { 56 | childChildren.push(); 64 | } else { 65 | childChildren[childChildren.length - 1] = (); 73 | } 74 | 75 | const newProps = { 76 | className: childClass, 77 | filteradded: 'true', 78 | }; 79 | 80 | const clonedChild = React.cloneElement(child, newProps, [...childChildren]); 81 | finalChildren.push(clonedChild); 82 | } else { 83 | if (!isUndefined(child)) { 84 | const clonedChild = React.cloneElement(child); 85 | finalChildren.push(clonedChild); 86 | } 87 | } 88 | }); 89 | } else { 90 | console.error('TableFilter Error: Should contain one or more children'); 91 | } 92 | 93 | let rowHtml; 94 | // The child columns will by default be placed inside component. If 'rowComponent' is 95 | // passed in via props it will be used 96 | if (!isUndefined(this.props.rowComponent)) { 97 | const rowComponent = this.props.rowComponent; 98 | const newProps = { 99 | className: [this.props.rowClass ? this.props.rowClass + ' ': '', 'table-filter-row'].join(''), 100 | }; 101 | const clonedRowComponent = React.cloneElement(rowComponent, newProps, [...finalChildren]); 102 | rowHtml = clonedRowComponent; 103 | } else { 104 | rowHtml = ( 105 | 106 | { finalChildren } 107 | 108 | ); 109 | } 110 | 111 | return rowHtml; 112 | }; 113 | 114 | export default render; 115 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 3 | var webpack = require('webpack'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | const analyze = process.env.analyze; 7 | 8 | let plugins = [ 9 | new ExtractTextPlugin({ 10 | filename: 'styles.css', 11 | disable: false, 12 | allChunks: true 13 | }), 14 | new webpack.optimize.AggressiveMergingPlugin()//Merge chunks 15 | ]; 16 | 17 | if (analyze) { 18 | plugins.push(new BundleAnalyzerPlugin()) 19 | } 20 | 21 | module.exports = { 22 | mode: 'production', 23 | entry: './src/index.js', 24 | output: { 25 | filename: 'bundle.js', 26 | path: path.resolve(__dirname, 'lib'), 27 | library: 'tableFilter', 28 | libraryTarget: 'umd' 29 | }, 30 | externals: { 31 | react: { 32 | root: 'React', 33 | commonjs2: 'react', 34 | commonjs: 'react', 35 | amd: 'react' 36 | }, 37 | 'react-dom': { 38 | root: 'ReactDOM', 39 | commonjs2: 'react-dom', 40 | commonjs: 'react-dom', 41 | amd: 'react-dom' 42 | } 43 | }, 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.js$/, 48 | use: { 49 | loader: 'babel-loader' 50 | } 51 | }, 52 | { 53 | test: /\.scss$/, 54 | use: ExtractTextPlugin.extract({ 55 | fallback: 'style-loader', 56 | use: ['css-loader', 'sass-loader'] 57 | }) 58 | } 59 | ] 60 | }, 61 | plugins: plugins, 62 | optimization: { 63 | minimizer: [ 64 | new TerserPlugin() 65 | ] 66 | }, 67 | }; 68 | --------------------------------------------------------------------------------