├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .travis.yml ├── example ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── DemoDropdown.jsx │ ├── index.css │ └── index.js ├── package-lock.json └── src ├── .eslintrc └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react" 9 | ], 10 | "env": { 11 | "node": true 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2020, 15 | "ecmaFeatures": { 16 | "legacyDecorators": true, 17 | "jsx": true 18 | } 19 | }, 20 | "settings": { 21 | "react": { 22 | "version": "16" 23 | } 24 | }, 25 | "rules": { 26 | "space-before-function-paren": 0, 27 | "react/prop-types": 0, 28 | "react/jsx-handler-names": 0, 29 | "react/jsx-fragments": 0, 30 | "react/no-unused-prop-types": 0, 31 | "import/export": 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mui-multiselect-dropdown-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start", 8 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build", 9 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test", 10 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject" 11 | }, 12 | "dependencies": { 13 | "react": "file:../node_modules/react", 14 | "@material-ui/core": "^4.11.0", 15 | "@material-ui/icons": "^4.9.1", 16 | "fontsource-roboto": "^2.1.4", 17 | "classnames": "^2.2.6", 18 | "react-dom": "file:../node_modules/react-dom", 19 | "react-scripts": "file:../node_modules/react-scripts", 20 | "react-mui-multiselect-dropdown": "file:.." 21 | }, 22 | "devDependencies": { 23 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherb-star/react-mui-multiselect-dropdown/334210c29d91009e7668ba55cbcee42b563ae969/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | 27 | react-mui-multiselect-dropdown 28 | 29 | 30 | 31 | 34 | 35 |
36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-mui-multiselect-dropdown", 3 | "name": "react-mui-multiselect-dropdown", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import DemoDropdown from './DemoDropdown' 4 | 5 | const App = () => { 6 | return 7 | } 8 | 9 | export default App 10 | -------------------------------------------------------------------------------- /example/src/DemoDropdown.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import Dropdown from 'react-mui-multiselect-dropdown' 3 | import { Paper, Grid, Typography, makeStyles, Button } from '@material-ui/core' 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | error: { 7 | color: theme.palette.error.dark, 8 | fontSize: '1em' 9 | }, 10 | checkBox: { 11 | color: 'Purple' 12 | } 13 | })) 14 | 15 | function DemoDropdown() { 16 | const [selectedEmployee, setSelectedEmployees] = useState([]) 17 | const [selectedSkills, setSelectedSkills] = useState([]) 18 | const [selectedCities, setSelectedCities] = useState([]) 19 | 20 | const [skills, setSkills] = useState([]) 21 | const [employees, setEmployees] = useState([]) 22 | const [cities, setCities] = useState([]) 23 | 24 | const populateData = () => { 25 | const employeesData = [ 26 | { id: 1, name: 'Bhushan' }, 27 | { id: 2, name: 'Vishal' }, 28 | { id: 3, name: 'Ravindra' } 29 | ] 30 | employees.forEach((v, i) => { 31 | v['path'] = `https://source.unsplash.com/random/${i}` 32 | }) 33 | 34 | setEmployees(employeesData) 35 | const skillsData = [ 36 | { id: 1, name: 'React Js' }, 37 | { id: 2, name: 'Angular' }, 38 | { id: 3, name: 'Node JS' } 39 | ] 40 | 41 | setSkills(skillsData) 42 | const SelectedEmp = [] 43 | setSelectedEmployees(SelectedEmp) 44 | 45 | const SelectedSkills = [] 46 | setSelectedSkills(SelectedSkills) 47 | 48 | const cities = [ 49 | { id: 1, city: 'MUMBAI' }, 50 | { id: 2, city: 'PUNE' }, 51 | { id: 3, city: 'NAGPUR' } 52 | ] 53 | 54 | cities.forEach((v, i) => { 55 | v['path'] = `https://source.unsplash.com/random/${i}` 56 | }) 57 | 58 | setCities(cities) 59 | } 60 | 61 | useEffect(() => { 62 | populateData() 63 | }, []) 64 | 65 | const classes = useStyles() 66 | 67 | return ( 68 | <> 69 |
70 | 71 | 72 | 73 | 77 | Single Select 78 | 79 | { 97 | setSelectedEmployees(records) 98 | }} 99 | onDeleteItem={(deleted) => { 100 | console.log('deleted', deleted) 101 | }} 102 | /> 103 | 107 | Multi Select 108 | 109 | { 129 | setSelectedSkills(records) 130 | }} 131 | onDeleteItem={(deleted) => { 132 | console.log('deleted', deleted) 133 | }} 134 | /> 135 | 139 | Select with Image 140 | 141 | { 159 | setSelectedCities(city) 160 | }} 161 | onDeleteItem={(deleted) => { 162 | console.log('deleted', deleted) 163 | }} 164 | /> 165 | 166 | 167 | 168 |
169 | 170 | ) 171 | } 172 | 173 | export default DemoDropdown 174 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react' 2 | import { 3 | Input, 4 | InputAdornment, 5 | ButtonBase, 6 | Menu, 7 | MenuItem, 8 | makeStyles, 9 | Chip, 10 | Icon, 11 | FormHelperText, 12 | Avatar, 13 | Typography 14 | } from '@material-ui/core' 15 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown' 16 | import PropTypes from 'prop-types' 17 | import ClearIcon from '@material-ui/icons/Clear' 18 | import CheckBoxOutlineBlankOutlinedIcon from '@material-ui/icons/CheckBoxOutlineBlankOutlined' 19 | import CheckBoxIcon from '@material-ui/icons/CheckBox' 20 | import classNames from 'classnames' 21 | 22 | const useStyles = makeStyles((theme) => ({ 23 | selectedItemsWrapper: { 24 | display: 'flex', 25 | flex: 1, 26 | flexWrap: 'wrap' 27 | }, 28 | selectedList: { 29 | listStyle: 'none', 30 | padding: '3px 0' 31 | }, 32 | checkBox: { 33 | color: theme.palette.primary.main 34 | }, 35 | chip: { 36 | marginRight: '0.5em' 37 | }, 38 | Menupaper: { 39 | overflowY: 'hidden !important' 40 | }, 41 | artwork: { 42 | width: '1em', 43 | height: '1em', 44 | marginLeft: '3px', 45 | marginRight: '5px' 46 | } 47 | })) 48 | 49 | function Dropdown(Props) { 50 | const [open, setOpen] = useState(false) 51 | const [filteredValues, setFilteredValues] = useState([]) 52 | const [selectedItems, setSelectedItems] = useState([]) 53 | const [searchKeyword, setSearchKeyword] = useState('') 54 | 55 | const inputRef = useRef() 56 | const wrapperRef = useRef() 57 | const classes = useStyles() 58 | 59 | const { 60 | fullWidth, 61 | title, 62 | searchPlaceHolder, 63 | searchable, 64 | data, 65 | onItemClick, 66 | itemId, 67 | itemLabel, 68 | multiple, 69 | showAllButton, 70 | simpleValue, 71 | itemValue, 72 | onDeleteItem, 73 | searchByValue, 74 | disabled, 75 | error, 76 | errorText, 77 | customStyles, 78 | selectedValues, 79 | showImage, 80 | imageLabel, 81 | allItemValue, 82 | allItemId, 83 | notFoundText 84 | } = Props 85 | 86 | useEffect(() => { 87 | if (data && data.length > 0 && multiple && showAllButton) { 88 | data.unshift({ [allItemId]: allItemValue, [itemLabel]: 'All' }) 89 | } 90 | const values = 91 | !multiple && selectedValues.length > 1 92 | ? selectedValues.filter((_, i) => i === 0) 93 | : selectedValues.concat() 94 | setSelectedItems(values) 95 | setFilteredValues(data) 96 | }, [data]) 97 | 98 | const handleClose = () => { 99 | setOpen(false) 100 | handleSearchKeyword('') 101 | } 102 | 103 | const onItemSelect = async (item) => { 104 | let selected = selectedItems 105 | let isAllSelected = false 106 | if (multiple) { 107 | if (item[allItemId] === allItemValue) { 108 | item = filteredValues.filter((v) => v[allItemId] !== allItemValue) 109 | isAllSelected = true 110 | } 111 | if ( 112 | isAllSelected && 113 | item.length === 114 | selectedItems.filter((v) => v[allItemId] !== allItemValue).length 115 | ) { 116 | selected = [] 117 | } else if (isItemSelected(item)) { 118 | selected = selected.filter((v) => v[itemId] !== item[itemId]) 119 | populateDeletedValue(item) 120 | } else { 121 | selected = isAllSelected ? item : [...selectedItems, item] 122 | } 123 | } else { 124 | selected = [item] 125 | } 126 | setSelectedItems(selected) 127 | populateSelectedValues(selected) 128 | if (!multiple) { 129 | handleClose() 130 | } 131 | } 132 | 133 | const handleRemoveItem = (item) => { 134 | const filteredSelectedItems = selectedItems.filter( 135 | (v) => v[itemId] !== item[itemId] 136 | ) 137 | setSelectedItems(filteredSelectedItems) 138 | populateSelectedValues(filteredSelectedItems) 139 | populateDeletedValue(item) 140 | } 141 | 142 | const removeAllSelectedItems = () => { 143 | setSelectedItems([]) 144 | populateSelectedValues([]) 145 | } 146 | 147 | const isItemSelected = (item) => { 148 | if ( 149 | item[allItemId] === allItemValue && 150 | selectedItems.length === 151 | data.filter((v) => v[allItemId] !== allItemValue).length 152 | ) { 153 | return true 154 | } 155 | return selectedItems.filter((v) => v[itemId] === item[itemId]).length > 0 156 | } 157 | 158 | const populateSelectedValues = (values) => { 159 | let valuesToPopulate 160 | 161 | if (values && values.length === 0) { 162 | valuesToPopulate = simpleValue ? null : [] 163 | } else { 164 | valuesToPopulate = simpleValue ? values.map((v) => v[itemValue]) : values 165 | valuesToPopulate = multiple ? valuesToPopulate : valuesToPopulate[0] 166 | } 167 | onItemClick(valuesToPopulate) 168 | } 169 | 170 | const populateDeletedValue = (item) => { 171 | const deletedValue = simpleValue ? item[itemValue] : item 172 | onDeleteItem(deletedValue) 173 | } 174 | 175 | const handleSearch = (keyword) => { 176 | keyword = keyword ? keyword.toLowerCase() : keyword 177 | let filtredResult = data.filter((v) => 178 | v[searchByValue].toString().toLowerCase().includes(keyword) 179 | ) 180 | const ignoreResult = data.filter((v) => v[allItemId] === allItemValue) 181 | if (JSON.stringify(filtredResult) === JSON.stringify(ignoreResult)) { 182 | filtredResult = [] 183 | } 184 | setFilteredValues(filtredResult) 185 | } 186 | 187 | const handleSearchKeyword = (keyword) => { 188 | setSearchKeyword(keyword) 189 | handleSearch(keyword) 190 | } 191 | 192 | const searchSection = () => 193 | searchable && ( 194 |
195 | { 200 | handleSearchKeyword(e.target.value) 201 | }} 202 | style={{ 203 | minWidth: wrapperRef.current 204 | ? wrapperRef.current.getBoundingClientRect().width 205 | : undefined, 206 | paddingLeft: '13px' 207 | }} 208 | endAdornment={ 209 | 210 | {searchKeyword && ( 211 | { 215 | setSearchKeyword('') 216 | handleSearchKeyword('') 217 | }} 218 | > 219 | 220 | 221 | )} 222 | 223 | } 224 | /> 225 |
226 | ) 227 | 228 | const optionsSection = () => ( 229 |
236 | {filteredValues && filteredValues.length > 0 ? ( 237 | filteredValues.map((v) => ( 238 | { 241 | onItemSelect(v) 242 | }} 243 | style={{ 244 | minWidth: wrapperRef.current 245 | ? wrapperRef.current.getBoundingClientRect().width 246 | : undefined 247 | }} 248 | > 249 | {multiple && ( 250 | 251 | {isItemSelected(v) ? ( 252 | 258 | ) : ( 259 | 265 | )} 266 | 267 | )} 268 | 269 | {showImage && 270 | (v[allItemId] !== allItemValue ? ( 271 | img 276 | ) : ( 277 | 281 | ))} 282 | {v[itemLabel]} 283 | 284 | 285 | )) 286 | ) : ( 287 | {notFoundText} 288 | )} 289 |
290 | ) 291 | 292 | const selectedOptionSetion = () => ( 293 |
{ 296 | e.stopPropagation() 297 | !disabled && setOpen(true) 298 | }} 299 | style={{ 300 | cursor: disabled ? '' : 'pointer', 301 | height: selectedItems && selectedItems.length > 0 ? 'auto' : '1.3rem' 302 | }} 303 | > 304 | {selectedItems && 305 | selectedItems.length > 0 && 306 | selectedItems.map((item) => ( 307 |
  • 308 | {multiple ? ( 309 | : undefined 312 | } 313 | className={classes.chip} 314 | label={item[itemLabel]} 315 | onDelete={() => { 316 | handleRemoveItem(item) 317 | }} 318 | /> 319 | ) : ( 320 | 321 | {showImage && ( 322 | img 327 | )} 328 | {item[itemLabel]} 329 | 330 | )} 331 |
  • 332 | ))} 333 |
    334 | ) 335 | 336 | return ( 337 |
    338 | {title} 339 | 349 | {selectedItems && selectedItems.length > 0 && ( 350 | { 354 | removeAllSelectedItems() 355 | }} 356 | > 357 | 358 | 359 | )} 360 | 361 | { 365 | setOpen(true) 366 | }} 367 | > 368 | 369 | 370 | 371 | } 372 | /> 373 | {error && errorText && ( 374 | 375 | {errorText} 376 | 377 | )} 378 | {open && ( 379 | { 392 | handleClose() 393 | }} 394 | > 395 | 396 | {searchSection()} 397 | {optionsSection()} 398 | 399 | 400 | )} 401 |
    402 | ) 403 | } 404 | 405 | Dropdown.propTypes = { 406 | fullWidth: PropTypes.bool, 407 | searchable: PropTypes.bool, 408 | title: PropTypes.string, 409 | searchPlaceHolder: PropTypes.string, 410 | data: PropTypes.array.isRequired, 411 | onItemClick: PropTypes.func.isRequired, 412 | itemLabel: PropTypes.string.isRequired, 413 | itemId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 414 | allItemId: PropTypes.string, 415 | allItemValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 416 | multiple: PropTypes.bool, 417 | showAllButton: PropTypes.bool, 418 | simpleValue: PropTypes.bool, 419 | itemValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 420 | onDeleteItem: PropTypes.func, 421 | searchByValue: PropTypes.string, 422 | disabled: PropTypes.bool, 423 | error: PropTypes.bool, 424 | showImage: PropTypes.bool, 425 | selectedValues: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), 426 | imageLabel: PropTypes.string, 427 | errorText: PropTypes.string, 428 | notFoundText: PropTypes.string, 429 | customStyles: PropTypes.shape({ 430 | checkBox: PropTypes.string, 431 | error: PropTypes.string 432 | }) 433 | } 434 | Dropdown.defaultProps = { 435 | fullWidth: false, 436 | searchable: false, 437 | title: 'Dropdown', 438 | searchPlaceHolder: 'Search result', 439 | itemId: 'id', 440 | allItemId: 'id', 441 | allItemValue: -1, 442 | multiple: false, 443 | showAllButton: true, 444 | simpleValue: false, 445 | itemValue: 'id', 446 | onDeleteItem: (a) => a, 447 | searchByValue: 'name', 448 | disabled: false, 449 | error: false, 450 | errorText: 'Error', 451 | selectedValues: [], 452 | showImage: false, 453 | imageLabel: 'url', 454 | notFoundText: 'Not Found', 455 | customStyles: { 456 | checkBox: '', 457 | error: '' 458 | } 459 | } 460 | 461 | export default Dropdown 462 | --------------------------------------------------------------------------------