├── .gitignore ├── images ├── dd.png ├── pr.png ├── build.png ├── cancel.png ├── circle.png ├── close.png ├── circle2.png └── license.png ├── .storybook ├── preview-head.html ├── preview.js └── main.js ├── _config.yml ├── .npmignore ├── src ├── index.tsx ├── assets │ └── svg │ │ ├── downArrow.svg │ │ ├── closeCircle.svg │ │ ├── closeSquare.svg │ │ ├── closeLine.svg │ │ └── closeCircleDark.svg └── multiselect │ ├── interface.ts │ ├── styles.css │ └── multiselect.component.tsx ├── stories ├── constants.ts ├── serverside.stories.tsx └── basic.stories.tsx ├── tsconfig.json ├── .github └── workflows │ └── npm-publish.yml ├── tsdx.config.js ├── LICENSE ├── webpack.config.js ├── _layouts └── default.html ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .sonarlint/ 3 | .vscode/ 4 | dist/ 5 | storybook-static/ -------------------------------------------------------------------------------- /images/dd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/dd.png -------------------------------------------------------------------------------- /images/pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/pr.png -------------------------------------------------------------------------------- /images/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/build.png -------------------------------------------------------------------------------- /images/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/cancel.png -------------------------------------------------------------------------------- /images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/circle.png -------------------------------------------------------------------------------- /images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/close.png -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/circle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/circle2.png -------------------------------------------------------------------------------- /images/license.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srigar/multiselect-react-dropdown/HEAD/images/license.png -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | name: Srigar Sukumar 3 | url: https://srigar.github.io/multiselect-react-dropdown/ 4 | plugins: 5 | - jekyll-seo-tag -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _layouts 2 | node_modules 3 | _config.yml 4 | webpack.config.js 5 | src 6 | images 7 | tsconfig.json 8 | tsdx.config.js 9 | .storybook 10 | stories 11 | .github -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Multiselect } from "./multiselect/multiselect.component"; 2 | 3 | // For backward compatability 4 | export { Multiselect }; 5 | 6 | export default Multiselect; -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | controls: { 5 | matchers: { 6 | color: /(background|color)$/i, 7 | date: /Date$/, 8 | }, 9 | }, 10 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../stories/**/*.stories.mdx", 4 | "../stories/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ], 10 | "typescript": { 11 | "check": true 12 | } 13 | } -------------------------------------------------------------------------------- /src/assets/svg/downArrow.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /stories/constants.ts: -------------------------------------------------------------------------------- 1 | export const flatArray = ["Option 1", "Option 2", "Option 3", "Option 4", "Option 5"]; 2 | 3 | export const options = [ 4 | { key: "Option 1", cat: "Group 1" }, 5 | { key: "Option 2", cat: "Group 1" }, 6 | { key: "Option 3", cat: "Group 1" }, 7 | { key: "Option 4", cat: "Group 2" }, 8 | { key: "Option 5", cat: "Group 2" }, 9 | { key: "Option 6", cat: "Group 2" }, 10 | { key: "Option 7", cat: "Group 2" } 11 | ]; 12 | 13 | export const selectedValues = [ 14 | { key: "Option 1", cat: "Group 1" }, 15 | { key: "Option 2", cat: "Group 1" } 16 | ]; -------------------------------------------------------------------------------- /src/assets/svg/closeCircle.svg: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": false, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "jsx": "react", 25 | "esModuleInterop": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.[0-9]+.[0-9]+ 10 | 11 | jobs: 12 | publish-npm: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: 14 19 | registry-url: https://registry.npmjs.org/ 20 | - run: npm install 21 | - run: npm run build-tsdx 22 | - run: npm publish 23 | env: 24 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 25 | -------------------------------------------------------------------------------- /src/assets/svg/closeSquare.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/svg/closeLine.svg: -------------------------------------------------------------------------------- 1 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | const postcss = require("rollup-plugin-postcss"); 2 | const url = require('@rollup/plugin-url') 3 | const svgr = require('@svgr/rollup').default; 4 | 5 | module.exports = { 6 | rollup(config, options) { 7 | config.plugins.push( 8 | postcss({ 9 | minimize: true, 10 | }) 11 | ); 12 | config.plugins = [ 13 | svgr({ 14 | // configure however you like, this is just an example 15 | ref: true, 16 | memo: true, 17 | svgoConfig: { 18 | plugins: [ 19 | { removeViewBox: false }, 20 | { removeAttrs: { attrs: 'g:(stroke|fill):((?!^none$).)*' } } 21 | ], 22 | }, 23 | }), 24 | // Force the `url` plugin to emit files. 25 | url({ include: ['**/*.ttf', '**/*.woff', '**/*.eot', '**/*.svg'] }), 26 | ...config.plugins, 27 | ] 28 | return config; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/assets/svg/closeCircleDark.svg: -------------------------------------------------------------------------------- 1 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Srigar Sukumar 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/multiselect/interface.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface IMultiselectProps { 4 | options: any, 5 | disablePreSelectedValues?: boolean, 6 | selectedValues?: any, 7 | isObject?: boolean, 8 | displayValue?: string, 9 | showCheckbox?: boolean, 10 | selectionLimit?: any, 11 | placeholder?: string, 12 | groupBy?: string, 13 | loading?: boolean, 14 | style?: object, 15 | emptyRecordMsg?: string, 16 | onSelect?: (selectedList:any, selectedItem: any) => void, 17 | onRemove?: (selectedList:any, selectedItem: any) => void, 18 | onSearch?: (value:string) => void, 19 | onKeyPressFn?: (event:any, value:string) => void, 20 | closeIcon?: string, 21 | singleSelect?: boolean, 22 | caseSensitiveSearch?: boolean, 23 | id?: string, 24 | closeOnSelect?: boolean, 25 | avoidHighlightFirstOption?: boolean, 26 | hidePlaceholder?: boolean, 27 | showArrow?: boolean, 28 | keepSearchTerm?: boolean, 29 | customCloseIcon?: React.ReactNode | string, 30 | customArrow?: any; 31 | disable?: boolean; 32 | className?: string; 33 | selectedValueDecorator?: (v:string, option: any) => React.ReactNode | string; 34 | optionValueDecorator?: (v:string, option: any) => React.ReactNode | string 35 | hideSelectedList?: boolean 36 | } 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: "./src/multiselect/multiselect.component.js", 8 | output: { 9 | path: path.resolve(__dirname, 'build'), 10 | filename: "index.js", 11 | libraryTarget: 'commonjs2' 12 | }, 13 | devServer: { 14 | contentBase: path.join(__dirname, "dist"), 15 | compress: true, 16 | port: 7000, 17 | watchContentBase: true, 18 | progress: true 19 | }, 20 | mode: "production", 21 | devtool: 'source-map', 22 | plugins: [new MiniCssExtractPlugin({ 23 | filename: 'styles.css', 24 | })], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.js$/, 29 | exclude: /(node_modules|bower_components|build|dist)/, 30 | use: { 31 | loader: "babel-loader" 32 | } 33 | }, 34 | { 35 | test: /\.css$/, 36 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 37 | }, 38 | { 39 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 40 | loader: 'url-loader?limit=100000' 41 | } 42 | ] 43 | }, 44 | optimization: { 45 | minimizer: [ 46 | new CssMinimizerPlugin({ 47 | sourceMap: false 48 | }), 49 | new TerserPlugin({ 50 | sourceMap: true 51 | }) 52 | ] 53 | }, 54 | externals: { 55 | 'react': 'commonjs react' 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /stories/serverside.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from "@storybook/addon-knobs"; 2 | import React, { useState } from "react"; 3 | import { Story, Meta } from '@storybook/react'; 4 | 5 | import MultiSelect from "../src"; 6 | import { IMultiselectProps } from "../src/multiselect/interface"; 7 | 8 | const style = { 9 | chips: { 10 | background: "red" 11 | }, 12 | searchBox: { 13 | border: "none", 14 | "border-bottom": "1px solid blue", 15 | "border-radius": "0px" 16 | }, 17 | multiselectContainer: { 18 | color: "red" 19 | } 20 | }; 21 | 22 | export default { 23 | title: "Serverside Retrieval", 24 | component: MultiSelect, 25 | decorators: [withKnobs] 26 | } as Meta; 27 | 28 | const Template: Story = (args) => ; 29 | 30 | 31 | const range = (size: number): Array => 32 | Array.from(new Array(size + 1).keys()).slice(1); 33 | 34 | const words = ["Car", "Bike", "E-Bike", "Bus", "Tram", "Truck"] 35 | const randInt = (max: number) => () => Math.floor(Math.random() * max) 36 | const nextIndex = randInt(words.length) 37 | const nextWord = () => words[nextIndex()] 38 | const data = range(100).map(i => `${nextWord()} ${i}`) 39 | 40 | 41 | export const AsynchronousLoading = () => { 42 | const [options_, setOptions] = useState([]); 43 | const [loading, setLoading] = useState(false); 44 | const handleSearch = (value) => { 45 | setLoading(true); 46 | const results = value ? data.filter(w => w.toLowerCase().includes(value)) : [] 47 | setTimeout(r => { setOptions(r); setLoading(false) }, 400, results) 48 | } 49 | return 55 | } 56 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Multiselect Dropdown 6 | {% if site.google_analytics %} 7 | 8 | 14 | {% endif %} 15 | 16 | 17 | {% seo %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | 36 |
37 | {{ content }} 38 | 39 | 45 |
46 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiselect-react-dropdown", 3 | "version": "2.0.24", 4 | "description": "React multiselect dropdown component with search and various features", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "dev": "webpack-dev-server", 9 | "build": "webpack", 10 | "build-tsdx": "tsdx build", 11 | "storybook": "start-storybook -p 6006", 12 | "build-storybook": "build-storybook" 13 | }, 14 | "author": "srigar s", 15 | "license": "MIT", 16 | "homepage": "https://multiselect-react-dropdown.vercel.app/?path=/docs/multiselect-dropdown--flat-array", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/srigar/multiselect-react-dropdown" 20 | }, 21 | "peerDependencies": { 22 | "react": "^16.7.0 || ^17.0.0 || ^18.0.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/cli": "7.14.5", 26 | "@babel/core": "^7.14.5", 27 | "@babel/preset-env": "^7.4.2", 28 | "@babel/preset-react": "^7.0.0", 29 | "@rollup/plugin-url": "^6.0.0", 30 | "@storybook/addon-actions": "^6.5.9", 31 | "@storybook/addon-controls": "^6.5.9", 32 | "@storybook/addon-essentials": "^6.5.9", 33 | "@storybook/addon-knobs": "^6.4.0", 34 | "@storybook/addon-links": "^6.5.9", 35 | "@storybook/react": "^6.5.9", 36 | "@svgr/rollup": "^5.5.0", 37 | "@types/react": "^16.7.0", 38 | "@types/react-dom": "^16.7.0", 39 | "babel-loader": "^8.0.5", 40 | "css-loader": "^2.1.1", 41 | "css-minimizer-webpack-plugin": "^1.3.0", 42 | "mini-css-extract-plugin": "1.3.0", 43 | "postcss": "8.4.14", 44 | "react": "^16.7.0", 45 | "react-dom": "^16.7.0", 46 | "rollup-plugin-postcss": "^4.0.0", 47 | "style-loader": "^2.0.0", 48 | "terser-webpack-plugin": "^4.2.3", 49 | "tsdx": "^0.14.1", 50 | "tslib": "^2.2.0", 51 | "typescript": "^4.3.2", 52 | "url-loader": "^1.1.2", 53 | "webpack": "^4.40.6", 54 | "webpack-cli": "^3.3.11", 55 | "webpack-dev-server": "^3.11.0" 56 | }, 57 | "babel": { 58 | "presets": [ 59 | "@babel/env", 60 | "@babel/react" 61 | ] 62 | }, 63 | "keywords": [ 64 | "npm", 65 | "react", 66 | "dropdown", 67 | "multiselect", 68 | "multilist", 69 | "list", 70 | "select", 71 | "component", 72 | "autocomplete", 73 | "multiselect-react-dropdown", 74 | "react-multiselect-dropdown", 75 | "multiselect react dropdown", 76 | "react multiselect dropdown" 77 | ], 78 | "resolutions": { 79 | "serialize-javascript": "3.1.0", 80 | "minimist": "1.2.3", 81 | "postcss": "8.2.10", 82 | "normalize-url": "4.5.1", 83 | "css-what": "5.0.1", 84 | "glob-parent": "5.1.2" 85 | }, 86 | "dependencies": {} 87 | } 88 | -------------------------------------------------------------------------------- /src/multiselect/styles.css: -------------------------------------------------------------------------------- 1 | .multiSelectContainer, 2 | .multiSelectContainer *, 3 | .multiSelectContainer ::after, 4 | .multiSelectContainer ::before { 5 | box-sizing: border-box; 6 | } 7 | 8 | .multiSelectContainer { 9 | position: relative; 10 | text-align: left; 11 | width: 100%; 12 | } 13 | .disable_ms { 14 | pointer-events: none; 15 | opacity: 0.5; 16 | } 17 | .display-none { 18 | display: none; 19 | } 20 | .searchWrapper { 21 | border: 1px solid #cccccc; 22 | border-radius: 4px; 23 | padding: 5px; 24 | min-height: 22px; 25 | position: relative; 26 | } 27 | .multiSelectContainer input { 28 | border: none; 29 | margin-top: 3px; 30 | background: transparent; 31 | } 32 | .multiSelectContainer input:focus { 33 | outline: none; 34 | } 35 | .chip { 36 | padding: 4px 10px; 37 | background: #0096fb; 38 | margin-right: 5px; 39 | margin-bottom: 5px; 40 | border-radius: 11px; 41 | display: inline-flex; 42 | align-items: center; 43 | font-size: 13px; 44 | line-height: 19px; 45 | color: #fff; 46 | white-space: nowrap; 47 | } 48 | .singleChip { 49 | background: none; 50 | border-radius: none; 51 | color: inherit; 52 | white-space: nowrap; 53 | } 54 | .singleChip i { 55 | display: none; 56 | } 57 | .closeIcon { 58 | height: 13px; 59 | width: 13px; 60 | float: right; 61 | margin-left: 5px; 62 | cursor: pointer; 63 | } 64 | .optionListContainer { 65 | position: absolute; 66 | width: 100%; 67 | background: #fff; 68 | border-radius: 4px; 69 | margin-top: 1px; 70 | z-index: 2; 71 | } 72 | .multiSelectContainer ul { 73 | display: block; 74 | padding: 0; 75 | margin: 0; 76 | border: 1px solid #ccc; 77 | border-radius: 4px; 78 | max-height: 250px; 79 | overflow-y: auto; 80 | } 81 | .multiSelectContainer li { 82 | padding: 10px 10px; 83 | } 84 | .multiSelectContainer li:hover { 85 | background: #0096fb; 86 | color: #fff; 87 | cursor: pointer; 88 | } 89 | .checkbox { 90 | margin-right: 10px; 91 | } 92 | .disableSelection { 93 | pointer-events: none; 94 | opacity: 0.5; 95 | } 96 | .highlightOption { 97 | background: #0096fb; 98 | color: #ffffff; 99 | } 100 | .displayBlock { 101 | display: block; 102 | } 103 | .displayNone { 104 | display: none; 105 | } 106 | .notFound { 107 | padding: 10px; 108 | display: block; 109 | } 110 | .singleSelect { 111 | padding-right: 20px; 112 | } 113 | li.groupHeading { 114 | color: #908e8e; 115 | pointer-events: none; 116 | padding: 5px 15px; 117 | } 118 | li.groupChildEle { 119 | padding-left: 30px; 120 | } 121 | .icon_down_dir { 122 | position: absolute; 123 | right: 10px; 124 | top: 50%; 125 | transform: translateY(-50%); 126 | width: 14px; 127 | } 128 | .icon_down_dir:before { 129 | content: '\e803'; 130 | } 131 | .custom-close { 132 | display: flex; 133 | } 134 | -------------------------------------------------------------------------------- /stories/basic.stories.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from "@storybook/addon-knobs"; 2 | import React, { useState } from "react"; 3 | import { Story, Meta } from '@storybook/react'; 4 | 5 | import MultiSelect from "../src"; 6 | import { options, flatArray, selectedValues } from "./constants"; 7 | import { IMultiselectProps } from "../src/multiselect/interface"; 8 | 9 | const style = { 10 | chips: { 11 | background: "red" 12 | }, 13 | searchBox: { 14 | border: "none", 15 | "border-bottom": "1px solid blue", 16 | "border-radius": "0px" 17 | }, 18 | multiselectContainer: { 19 | color: "red" 20 | } 21 | }; 22 | 23 | export default { 24 | title: "Multiselect Dropdown", 25 | component: MultiSelect, 26 | decorators: [withKnobs] 27 | } as Meta; 28 | 29 | const Template: Story = (args) => ; 30 | 31 | export const FlatArray = Template.bind({}); 32 | FlatArray.args = { 33 | options: flatArray, 34 | isObject: false 35 | }; 36 | 37 | export const ArrrayOfObjects = Template.bind({}); 38 | ArrrayOfObjects.args = { 39 | options, 40 | displayValue: 'key' 41 | }; 42 | 43 | export const SelectedValueDecorator = Template.bind({}); 44 | SelectedValueDecorator.args = { 45 | options: [{key: "Option 0 is extremely long and therefore should probably be shortened once selected as a value", cat: "Group 1"},...options], 46 | selectedValueDecorator: v => v.length > 15 ? `${v.substring(0,15)}...` : v, 47 | displayValue: 'key' 48 | } as IMultiselectProps; 49 | 50 | export const OptionValueDecorator = Template.bind({}); 51 | OptionValueDecorator.args = { 52 | options, 53 | optionValueDecorator: v => v.toUpperCase(), 54 | displayValue: 'key' 55 | } as IMultiselectProps; 56 | 57 | 58 | export const PreselectedValues = Template.bind({}); 59 | PreselectedValues.args = { 60 | options, 61 | displayValue: 'key', 62 | selectedValues: selectedValues 63 | }; 64 | 65 | export const DisablePreselectedValues = Template.bind({}); 66 | DisablePreselectedValues.args = { 67 | options, 68 | displayValue: 'key', 69 | disablePreSelectedValues: true, 70 | selectedValues: selectedValues 71 | }; 72 | 73 | export const ShowCheckbox = Template.bind({}); 74 | ShowCheckbox.args = { 75 | options, 76 | displayValue: 'key', 77 | showCheckbox: true 78 | }; 79 | 80 | export const HideSelectedList = Template.bind({}); 81 | HideSelectedList.args = { 82 | options, 83 | displayValue: 'key', 84 | showCheckbox: true, 85 | hideSelectedList: true 86 | }; 87 | 88 | export const Grouping = Template.bind({}); 89 | Grouping.args = { 90 | options, 91 | displayValue: 'key', 92 | showCheckbox: true, 93 | groupBy: "cat" 94 | }; 95 | 96 | export const SelectionLimit = Template.bind({}); 97 | SelectionLimit.args = { 98 | options, 99 | displayValue: 'key', 100 | selectionLimit: 2 101 | }; 102 | 103 | export const NormalSingleSelect = Template.bind({}); 104 | NormalSingleSelect.args = { 105 | options, 106 | displayValue: 'key', 107 | singleSelect: true 108 | }; 109 | 110 | export const CustomPlaceholder = Template.bind({}); 111 | CustomPlaceholder.args = { 112 | options, 113 | placeholder: "Custom Placeholder", 114 | displayValue: 'key', 115 | }; 116 | 117 | export const CssCustomization = Template.bind({}); 118 | CssCustomization.args = { 119 | options, 120 | placeholder: "CSS Custom", 121 | displayValue: 'key', 122 | id: "css_custom", 123 | style: style 124 | }; 125 | 126 | export const CustomCloseIcon = Template.bind({}); 127 | CustomCloseIcon.args = { 128 | options, 129 | displayValue: 'key', 130 | customCloseIcon: <>🍑, 131 | selectedValues 132 | }; 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REACT MULTISELECT DROPDOWN 2 | 3 | [![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@master/badge/badge-storybook.svg)](https://multiselect-react-dropdown.vercel.app/?path=/docs/multiselect-dropdown--flat-array) 4 | ![Version](https://img.shields.io/npm/v/multiselect-react-dropdown.svg) 5 | ![Downloads](https://img.shields.io/npm/dw/multiselect-react-dropdown.svg) 6 | ![License](https://img.shields.io/npm/l/multiselect-react-dropdown.svg) 7 | [![gzip](https://badgen.net/bundlephobia/minzip/multiselect-react-dropdown@2.0.1)](https://bundlephobia.com/result?p=multiselect-react-dropdown@2.0.1) 8 | ![Tweet](https://img.shields.io/twitter/url/https/twitter.com/ssrigar.svg?style=social) 9 | 10 | 💥💥💥 **React Library for Component Lazyloading. Tiny and Efficient. [Check it Out](https://github.com/srigar/react-lazyloading)** 💥💥💥 11 | 12 | 13 | ## Description 14 | 15 | A React component which provides multi select functionality with various features like selection limit, CSS customization, checkbox, search option, disable preselected values, flat array, keyboard navigation for accessibility and grouping features. Also it has feature to behave like normal dropdown(means single select dropdown). 16 | 17 | ![Multiselect](images/dd.png) 18 | 19 | 20 | ## 🎉🎉 New features in >=2.0.0 21 | ✨ SSR Support 22 | 🍃 Light weight 23 | 🚀 Typescript 24 | 25 | 26 | 27 | ## 🏳️‍🌈 Getting Started 28 | 29 | ## 1. Installation 🔧 30 | ``` 31 | npm install multiselect-react-dropdown 32 | 33 | yarn add multiselect-react-dropdown 34 | ``` 35 | ---- 36 | ## 2. Demo 👁️ 37 | [React-multi-select-dropdown](https://multiselect-react-dropdown.vercel.app/?path=/docs/multiselect-dropdown--flat-array) 38 | 39 | ---- 40 | ## 3. Basic Usage 📑 41 | ```js 42 | import Multiselect from 'multiselect-react-dropdown'; 43 | 44 | this.state = { 45 | options: [{name: 'Option 1️⃣', id: 1},{name: 'Option 2️⃣', id: 2}] 46 | }; 47 | 48 | 55 | 56 | onSelect(selectedList, selectedItem) { 57 | ... 58 | } 59 | 60 | onRemove(selectedList, removedItem) { 61 | ... 62 | } 63 | ``` 64 | 65 | ---- 66 | 67 | ## 4. Props 💬 68 | 69 | | Prop | Type | Default | Description | 70 | |:--------- | :---- | :---- |:---- | 71 | | `options` | `array` | `[]` | Dropdown options 72 | | `onSelect` | `function` | `func` | Callback function will invoked on select event. Params are selectedList & selectedItem 73 | | `onRemove` | `function` | `func` | Callback function will invoked on remove event. Params are selectedList & removedItem 74 | | `singleSelect` | `boolean` | `false` | Make it `true` to behave like a normal dropdown(single select dropdown) 75 | | `selectedValues` | `array` | `[]` | Preselected value to persist in dropdown 76 | | `showCheckbox` | `bool` | `false` | To display checkbox option in the dropdown 77 | | `selectionLimit` | `number` | `-1` | You can limit the number of items that can be selected in a dropdown 78 | | `placeholder` | `string` | `Select` | Placeholder text 79 | | `disablePreSelectedValues` | `bool` | `false` | Prevent to deselect the preselected values 80 | | `isObject` | `bool` | `true` | Make it false to display flat array of string or number `Ex. ['Test1',1]` 81 | | `displayValue` | `string` | `value` | Property name in the object to display in the dropdown. Refer `Basic Usage` section 82 | | `emptyRecordMsg` | `string` | `No options available` | Message to display when no records found 83 | | `groupBy` | `string` | `''` | Group the popup list items with the corresponding category by the property name in the object 84 | | `closeIcon` | `string` | `circle` | Option to select close icon instead of default. Refer `Close Icon` section 85 | | `style` | `object` | `{}` | CSS Customization for multiselect. Refer below object for css customization. 86 | | `caseSensitiveSearch` | `bool` | `false` | Enables case sensitivity on the search field. 87 | | `closeOnSelect` | `bool` | `true` | Dropdown get closed on select/remove item from options. 88 | | `id` | `string` | `''` | Id for the multiselect container and input field(In input field it will append '{id}_input'). 89 | | `className` | `string` | `''` | Class for the multiselect container wrapper. 90 | | `avoidHighlightFirstOption` | `bool` | `false` | Based on flag first option will get highlight whenever optionlist open. 91 | | `hidePlaceholder` | `bool` | `false` | For true, placeholder will be hidden if there is any selected values in multiselect 92 | | `disable` | `bool` | `false` | For true, dropdown will be disabled 93 | | `onSearch` | `function` | `func` | Callback function invoked on search in multiselect, helpful to make api call to load data from api based on search. 94 | | `loading` | `bool` | `false` | If options is fetching from api, in the meantime, we can show `loading...` message in the list. 95 | | `loadingMessage` | `any` | `''` | Custom loading message, it can be string or component. 96 | | `showArrow` | `bool` | `false` | For multiselect dropdown by default arrow wont show at the end, If required based on flag we can display 97 | | `customArrow` | `any` | `undefined` | For multiselect dropdown custom arrow option 98 | | `keepSearchTerm` | `bool` | `false` | Whether or not to keep the search value after selecting or removing an item 99 | | `customCloseIcon` | `ReactNode or string` | `undefined` | Custom close icon and can be string or react component(Check demo for reference) 100 | | `selectedValueDecorator` | `(string) => ReactNode \| string` | `v => v` | A function that can be used to modify the representation selected value 101 | | `optionValueDecorator` | `(string) => string` | `v => v` | A function that can be used to modify the representation the available options 102 | ---- 103 | 104 | 105 | 106 | 107 | # 5. `Ref` as a prop 📌 108 | 109 | By using React.createRef() or useRef(), able to access below methods to get or reset selected values 110 | 111 | | Method Name | Description | 112 | |:--------- | :---- | 113 | | `resetSelectedValues` | Programatically reset selected values and returns promise 114 | | `getSelectedItems` | Get all selected items 115 | | `getSelectedItemsCount` | Get selected items count 116 | 117 | ```js 118 | constructor() { 119 | this.multiselectRef = React.createRef(); 120 | } 121 | 122 | resetValues() { 123 | // By calling the belowe method will reset the selected values programatically 124 | this.multiselectRef.current.resetSelectedValues(); 125 | } 126 | 127 | 131 | 132 | ``` 133 | 134 | ---- 135 | 136 | ## 6. CSS Customization 🌈 137 | 138 | ```css 139 | { 140 | multiselectContainer: { // To change css for multiselect (Width,height,etc..) 141 | .... 142 | }, 143 | searchBox: { // To change search box element look 144 | border: none; 145 | font-size: 10px; 146 | min-height: 50px; 147 | }, 148 | inputField: { // To change input field position or margin 149 | margin: 5px; 150 | }, 151 | chips: { // To change css chips(Selected options) 152 | background: red; 153 | }, 154 | optionContainer: { // To change css for option container 155 | border: 2px solid; 156 | } 157 | option: { // To change css for dropdown options 158 | color: blue; 159 | }, 160 | groupHeading: { // To chanage group heading style 161 | .... 162 | } 163 | } 164 | ``` 165 | 166 | ---- 167 | 168 | ## 7. Close Icons ❌ 169 | 170 | | Name | Image | 171 | |:--------- | :---- | 172 | | `circle` | ![Close Icon](images/circle.png) 173 | | `circle2` | ![Close Icon](images/circle2.png) 174 | | `cancel` | ![Close Icon](images/cancel.png) 175 | | `close` | ![Close Icon](images/close.png) 176 | 177 | ---- 178 | 179 | ## 8. Licence 📜 180 | MIT 181 | -------------------------------------------------------------------------------- /src/multiselect/multiselect.component.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React, { useRef, useEffect } from "react"; 3 | import "./styles.css"; 4 | import CloseCircle from '../assets/svg/closeCircle.svg'; 5 | import CloseCircleDark from '../assets/svg/closeCircleDark.svg'; 6 | import CloseLine from '../assets/svg/closeLine.svg'; 7 | import CloseSquare from '../assets/svg/closeSquare.svg'; 8 | import DownArrow from '../assets/svg/downArrow.svg'; 9 | import {IMultiselectProps} from "./interface"; 10 | 11 | const closeIconTypes = { 12 | circle: CloseCircleDark, 13 | circle2: CloseCircle, 14 | close: CloseSquare, 15 | cancel: CloseLine 16 | }; 17 | 18 | function useOutsideAlerter(ref, clickEvent) { 19 | useEffect(() => { 20 | function handleClickOutside(event) { 21 | if (ref.current && !ref.current.contains(event.target)) { 22 | clickEvent(); 23 | } 24 | } 25 | 26 | document.addEventListener("mousedown", handleClickOutside); 27 | return () => { 28 | document.removeEventListener("mousedown", handleClickOutside); 29 | }; 30 | }, [ref]); 31 | } 32 | 33 | /** 34 | * Component that alerts if you click outside of it 35 | */ 36 | function OutsideAlerter(props) { 37 | const wrapperRef = useRef(null); 38 | useOutsideAlerter(wrapperRef, props.outsideClick); 39 | return
{props.children}
; 40 | } 41 | 42 | export class Multiselect extends React.Component { 43 | static defaultProps: IMultiselectProps; 44 | constructor(props) { 45 | super(props); 46 | this.state = { 47 | inputValue: "", 48 | options: props.options, 49 | filteredOptions: props.options, 50 | unfilteredOptions: props.options, 51 | selectedValues: Object.assign([], props.selectedValues), 52 | preSelectedValues: Object.assign([], props.selectedValues), 53 | toggleOptionsList: false, 54 | highlightOption: props.avoidHighlightFirstOption ? -1 : 0, 55 | showCheckbox: props.showCheckbox, 56 | keepSearchTerm: props.keepSearchTerm, 57 | groupedObject: [], 58 | closeIconType: closeIconTypes[props.closeIcon] || closeIconTypes['circle'] 59 | }; 60 | // @ts-ignore 61 | this.optionTimeout = null; 62 | // @ts-ignore 63 | this.searchWrapper = React.createRef(); 64 | // @ts-ignore 65 | this.searchBox = React.createRef(); 66 | this.onChange = this.onChange.bind(this); 67 | this.onKeyPress = this.onKeyPress.bind(this); 68 | this.onFocus = this.onFocus.bind(this); 69 | this.onBlur = this.onBlur.bind(this); 70 | this.renderMultiselectContainer = this.renderMultiselectContainer.bind(this); 71 | this.renderSelectedList = this.renderSelectedList.bind(this); 72 | this.onRemoveSelectedItem = this.onRemoveSelectedItem.bind(this); 73 | this.toggelOptionList = this.toggelOptionList.bind(this); 74 | this.onArrowKeyNavigation = this.onArrowKeyNavigation.bind(this); 75 | this.onSelectItem = this.onSelectItem.bind(this); 76 | this.filterOptionsByInput = this.filterOptionsByInput.bind(this); 77 | this.removeSelectedValuesFromOptions = this.removeSelectedValuesFromOptions.bind(this); 78 | this.isSelectedValue = this.isSelectedValue.bind(this); 79 | this.fadeOutSelection = this.fadeOutSelection.bind(this); 80 | this.isDisablePreSelectedValues = this.isDisablePreSelectedValues.bind(this); 81 | this.renderGroupByOptions = this.renderGroupByOptions.bind(this); 82 | this.renderNormalOption = this.renderNormalOption.bind(this); 83 | this.listenerCallback = this.listenerCallback.bind(this); 84 | this.resetSelectedValues = this.resetSelectedValues.bind(this); 85 | this.getSelectedItems = this.getSelectedItems.bind(this); 86 | this.getSelectedItemsCount = this.getSelectedItemsCount.bind(this); 87 | this.hideOnClickOutside = this.hideOnClickOutside.bind(this); 88 | this.onCloseOptionList = this.onCloseOptionList.bind(this); 89 | this.isVisible = this.isVisible.bind(this); 90 | } 91 | 92 | initialSetValue() { 93 | const { showCheckbox, groupBy, singleSelect } = this.props; 94 | const { options } = this.state; 95 | if (!showCheckbox && !singleSelect) { 96 | this.removeSelectedValuesFromOptions(false); 97 | } 98 | // if (singleSelect) { 99 | // this.hideOnClickOutside(); 100 | // } 101 | if (groupBy) { 102 | this.groupByOptions(options); 103 | } 104 | } 105 | 106 | resetSelectedValues() { 107 | const { unfilteredOptions } = this.state; 108 | return new Promise((resolve) => { 109 | this.setState({ 110 | selectedValues: [], 111 | preSelectedValues: [], 112 | options: unfilteredOptions, 113 | filteredOptions: unfilteredOptions 114 | }, () => { 115 | // @ts-ignore 116 | resolve(); 117 | this.initialSetValue(); 118 | }); 119 | }); 120 | } 121 | 122 | getSelectedItems() { 123 | return this.state.selectedValues; 124 | } 125 | 126 | getSelectedItemsCount() { 127 | return this.state.selectedValues.length; 128 | } 129 | 130 | componentDidMount() { 131 | this.initialSetValue(); 132 | // @ts-ignore 133 | this.searchWrapper.current.addEventListener("click", this.listenerCallback); 134 | } 135 | 136 | componentDidUpdate(prevProps) { 137 | const { options, selectedValues } = this.props; 138 | const { options: prevOptions, selectedValues: prevSelectedvalues } = prevProps; 139 | if (JSON.stringify(prevOptions) !== JSON.stringify(options)) { 140 | this.setState({ options, filteredOptions: options, unfilteredOptions: options }, this.initialSetValue); 141 | } 142 | if (JSON.stringify(prevSelectedvalues) !== JSON.stringify(selectedValues)) { 143 | this.setState({ selectedValues: Object.assign([], selectedValues), preSelectedValues: Object.assign([], selectedValues) }, this.initialSetValue); 144 | } 145 | } 146 | 147 | listenerCallback() { 148 | // @ts-ignore 149 | this.searchBox.current.focus(); 150 | } 151 | 152 | componentWillUnmount() { 153 | // @ts-ignore 154 | if (this.optionTimeout) { 155 | // @ts-ignore 156 | clearTimeout(this.optionTimeout); 157 | } 158 | // @ts-ignore 159 | this.searchWrapper.current.removeEventListener('click', this.listenerCallback); 160 | } 161 | 162 | // Skipcheck flag - value will be true when the func called from on deselect anything. 163 | removeSelectedValuesFromOptions(skipCheck) { 164 | const { isObject, displayValue, groupBy } = this.props; 165 | const { selectedValues = [], unfilteredOptions, options } = this.state; 166 | if (!skipCheck && groupBy) { 167 | this.groupByOptions(options); 168 | } 169 | if (!selectedValues.length && !skipCheck) { 170 | return; 171 | } 172 | if (isObject) { 173 | let optionList = unfilteredOptions.filter(item => { 174 | return selectedValues.findIndex( 175 | v => v[displayValue] === item[displayValue] 176 | ) === -1 177 | ? true 178 | : false; 179 | }); 180 | if (groupBy) { 181 | this.groupByOptions(optionList); 182 | } 183 | this.setState( 184 | { options: optionList, filteredOptions: optionList }, 185 | this.filterOptionsByInput 186 | ); 187 | return; 188 | } 189 | let optionList = unfilteredOptions.filter( 190 | item => selectedValues.indexOf(item) === -1 191 | ); 192 | 193 | this.setState( 194 | { options: optionList, filteredOptions: optionList }, 195 | this.filterOptionsByInput 196 | ); 197 | } 198 | 199 | groupByOptions(options) { 200 | const { groupBy } = this.props; 201 | const groupedObject = options.reduce(function(r, a) { 202 | const key = a[groupBy] || "Others"; 203 | r[key] = r[key] || []; 204 | r[key].push(a); 205 | return r; 206 | }, Object.create({})); 207 | 208 | this.setState({ groupedObject }); 209 | } 210 | 211 | onChange(event) { 212 | const { onSearch } = this.props; 213 | this.setState( 214 | { inputValue: event.target.value }, 215 | this.filterOptionsByInput 216 | ); 217 | if (onSearch) { 218 | onSearch(event.target.value); 219 | } 220 | } 221 | 222 | onKeyPress(event) { 223 | const { onKeyPressFn } = this.props; 224 | if (onKeyPressFn) { 225 | onKeyPressFn(event, event.target.value); 226 | } 227 | } 228 | 229 | filterOptionsByInput() { 230 | let { options, filteredOptions, inputValue } = this.state; 231 | const { isObject, displayValue } = this.props; 232 | if (isObject) { 233 | options = filteredOptions.filter(i => this.matchValues(i[displayValue], inputValue)) 234 | } else { 235 | options = filteredOptions.filter(i => this.matchValues(i, inputValue)); 236 | } 237 | this.groupByOptions(options); 238 | this.setState({ options }); 239 | } 240 | 241 | matchValues(value, search) { 242 | if (this.props.caseSensitiveSearch) { 243 | return value.indexOf(search) > -1; 244 | } 245 | if (value.toLowerCase) { 246 | return value.toLowerCase().indexOf(search.toLowerCase()) > -1; 247 | } 248 | return value.toString().indexOf(search) > -1; 249 | } 250 | 251 | onArrowKeyNavigation(e) { 252 | const { 253 | options, 254 | highlightOption, 255 | toggleOptionsList, 256 | inputValue, 257 | selectedValues 258 | } = this.state; 259 | const { disablePreSelectedValues } = this.props; 260 | if (e.keyCode === 8 && !inputValue && !disablePreSelectedValues && selectedValues.length) { 261 | this.onRemoveSelectedItem(selectedValues.length - 1); 262 | } 263 | if (!options.length) { 264 | return; 265 | } 266 | if (e.keyCode === 38) { 267 | if (highlightOption > 0) { 268 | this.setState(previousState => ({ 269 | highlightOption: previousState.highlightOption - 1 270 | })); 271 | } else { 272 | this.setState({ highlightOption: options.length - 1 }); 273 | } 274 | } else if (e.keyCode === 40) { 275 | if (highlightOption < options.length - 1) { 276 | this.setState(previousState => ({ 277 | highlightOption: previousState.highlightOption + 1 278 | })); 279 | } else { 280 | this.setState({ highlightOption: 0 }); 281 | } 282 | } else if (e.key === "Enter" && options.length && toggleOptionsList) { 283 | if (highlightOption === -1) { 284 | return; 285 | } 286 | this.onSelectItem(options[highlightOption]); 287 | } 288 | // TODO: Instead of scrollIntoView need to find better soln for scroll the dropwdown container. 289 | // setTimeout(() => { 290 | // const element = document.querySelector("ul.optionContainer .highlight"); 291 | // if (element) { 292 | // element.scrollIntoView(); 293 | // } 294 | // }); 295 | } 296 | 297 | onRemoveSelectedItem(item) { 298 | let { selectedValues, index = 0 } = this.state; 299 | const { onRemove, showCheckbox, displayValue, isObject } = this.props; 300 | if (isObject) { 301 | index = selectedValues.findIndex( 302 | i => i[displayValue] === item[displayValue] 303 | ); 304 | } else { 305 | index = selectedValues.indexOf(item); 306 | } 307 | selectedValues.splice(index, 1); 308 | onRemove(selectedValues, item); 309 | this.setState({ selectedValues }, () => { 310 | if (!showCheckbox) { 311 | this.removeSelectedValuesFromOptions(true); 312 | } 313 | }); 314 | if (!this.props.closeOnSelect) { 315 | // @ts-ignore 316 | this.searchBox.current.focus(); 317 | } 318 | } 319 | 320 | onSelectItem(item) { 321 | const { selectedValues } = this.state; 322 | const { selectionLimit, onSelect, singleSelect, showCheckbox } = this.props; 323 | if (!this.state.keepSearchTerm){ 324 | this.setState({ 325 | inputValue: '' 326 | }); 327 | } 328 | if (singleSelect) { 329 | this.onSingleSelect(item); 330 | onSelect([item], item); 331 | return; 332 | } 333 | if (this.isSelectedValue(item)) { 334 | this.onRemoveSelectedItem(item); 335 | return; 336 | } 337 | if (selectionLimit == selectedValues.length) { 338 | return; 339 | } 340 | selectedValues.push(item); 341 | onSelect(selectedValues, item); 342 | this.setState({ selectedValues }, () => { 343 | if (!showCheckbox) { 344 | this.removeSelectedValuesFromOptions(true); 345 | } else { 346 | this.filterOptionsByInput(); 347 | } 348 | }); 349 | if (!this.props.closeOnSelect) { 350 | // @ts-ignore 351 | this.searchBox.current.focus(); 352 | } 353 | } 354 | 355 | onSingleSelect(item) { 356 | this.setState({ selectedValues: [item], toggleOptionsList: false }); 357 | } 358 | 359 | isSelectedValue(item) { 360 | const { isObject, displayValue } = this.props; 361 | const { selectedValues } = this.state; 362 | if (isObject) { 363 | return ( 364 | selectedValues.filter(i => i[displayValue] === item[displayValue]) 365 | .length > 0 366 | ); 367 | } 368 | return selectedValues.filter(i => i === item).length > 0; 369 | } 370 | 371 | renderOptionList() { 372 | const { groupBy, style, emptyRecordMsg, loading, loadingMessage = 'loading...' } = this.props; 373 | const { options } = this.state; 374 | if (loading) { 375 | return ( 376 |
    377 | {typeof loadingMessage === 'string' && {loadingMessage}} 378 | {typeof loadingMessage !== 'string' && loadingMessage} 379 |
380 | ); 381 | } 382 | return ( 383 |
    384 | {options.length === 0 && {emptyRecordMsg}} 385 | {!groupBy ? this.renderNormalOption() : this.renderGroupByOptions()} 386 |
387 | ); 388 | } 389 | 390 | renderGroupByOptions() { 391 | const { isObject = false, displayValue, showCheckbox, style, singleSelect } = this.props; 392 | const { groupedObject } = this.state; 393 | return Object.keys(groupedObject).map(obj => { 394 | return ( 395 | 396 |
  • {obj}
  • 397 | {groupedObject[obj].map((option, i) => { 398 | const isSelected = this.isSelectedValue(option); 399 | return ( 400 |
  • this.onSelectItem(option)} 405 | > 406 | {showCheckbox && !singleSelect && ( 407 | 413 | )} 414 | {this.props.optionValueDecorator(isObject ? option[displayValue] : (option || '').toString(), option)} 415 |
  • 416 | )} 417 | )} 418 |
    419 | ) 420 | }); 421 | } 422 | 423 | renderNormalOption() { 424 | const { isObject = false, displayValue, showCheckbox, style, singleSelect } = this.props; 425 | const { highlightOption } = this.state; 426 | return this.state.options.map((option, i) => { 427 | const isSelected = this.isSelectedValue(option); 428 | return ( 429 |
  • this.onSelectItem(option)} 434 | > 435 | {showCheckbox && !singleSelect && ( 436 | 442 | )} 443 | {this.props.optionValueDecorator(isObject ? option[displayValue] : (option || '').toString(), option)} 444 |
  • 445 | ) 446 | }); 447 | } 448 | 449 | renderSelectedList() { 450 | const { isObject = false, displayValue, style, singleSelect, customCloseIcon } = this.props; 451 | const { selectedValues, closeIconType } = this.state; 452 | return selectedValues.map((value, index) => ( 453 | 454 | {this.props.selectedValueDecorator(!isObject ? (value || '').toString() : value[displayValue], value)} 455 | {!this.isDisablePreSelectedValues(value) && (!customCloseIcon ? this.onRemoveSelectedItem(value)} 459 | /> : this.onRemoveSelectedItem(value)}>{customCloseIcon})} 460 | 461 | )); 462 | } 463 | 464 | isDisablePreSelectedValues(value) { 465 | const { isObject, disablePreSelectedValues, displayValue } = this.props; 466 | const { preSelectedValues } = this.state; 467 | if (!disablePreSelectedValues || !preSelectedValues.length) { 468 | return false; 469 | } 470 | if (isObject) { 471 | return ( 472 | preSelectedValues.filter(i => i[displayValue] === value[displayValue]) 473 | .length > 0 474 | ); 475 | } 476 | return preSelectedValues.filter(i => i === value).length > 0; 477 | } 478 | 479 | fadeOutSelection(item) { 480 | const { selectionLimit, showCheckbox, singleSelect } = this.props; 481 | if (singleSelect) { 482 | return; 483 | } 484 | const { selectedValues } = this.state; 485 | if (selectionLimit == -1) { 486 | return false; 487 | } 488 | if (selectionLimit != selectedValues.length) { 489 | return false; 490 | } 491 | if (selectionLimit == selectedValues.length) { 492 | if (!showCheckbox) { 493 | return true; 494 | } else { 495 | if (this.isSelectedValue(item)) { 496 | return false; 497 | } 498 | return true; 499 | } 500 | } 501 | } 502 | 503 | toggelOptionList() { 504 | this.setState({ 505 | toggleOptionsList: !this.state.toggleOptionsList, 506 | highlightOption: this.props.avoidHighlightFirstOption ? -1 : 0 507 | }); 508 | } 509 | 510 | onCloseOptionList() { 511 | this.setState({ 512 | toggleOptionsList: false, 513 | highlightOption: this.props.avoidHighlightFirstOption ? -1 : 0, 514 | inputValue: '' 515 | }); 516 | } 517 | 518 | onFocus(){ 519 | if (this.state.toggleOptionsList) { 520 | // @ts-ignore 521 | clearTimeout(this.optionTimeout); 522 | } else { 523 | this.toggelOptionList(); 524 | } 525 | } 526 | 527 | onBlur(){ 528 | this.setState({ inputValue: '' }, this.filterOptionsByInput); 529 | // @ts-ignore 530 | this.optionTimeout = setTimeout(this.onCloseOptionList, 250); 531 | } 532 | 533 | isVisible(elem) { 534 | return !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ) 535 | } 536 | 537 | hideOnClickOutside() { 538 | const element = document.getElementsByClassName('multiselect-container')[0]; 539 | const outsideClickListener = event => { 540 | if (element && !element.contains(event.target) && this.isVisible(element)) { 541 | this.toggelOptionList(); 542 | } 543 | } 544 | document.addEventListener('click', outsideClickListener) 545 | } 546 | 547 | renderMultiselectContainer() { 548 | const { inputValue, toggleOptionsList, selectedValues } = this.state; 549 | const { placeholder, style, singleSelect, id, name, hidePlaceholder, disable, showArrow, className, customArrow, hideSelectedList } = this.props; 550 | return ( 551 |
    552 |
    {}} 555 | > 556 | {!hideSelectedList && this.renderSelectedList()} 557 | 574 | {(singleSelect || showArrow) && ( 575 | <> 576 | {customArrow ? {customArrow} : } 577 | 578 | )} 579 |
    580 |
    { 585 | e.preventDefault(); 586 | }} 587 | > 588 | {this.renderOptionList()} 589 |
    590 |
    591 | ); 592 | } 593 | 594 | render() { 595 | return ( 596 | 597 | {this.renderMultiselectContainer()} 598 | 599 | ); 600 | } 601 | } 602 | 603 | Multiselect.defaultProps = { 604 | options: [], 605 | disablePreSelectedValues: false, 606 | selectedValues: [], 607 | isObject: true, 608 | displayValue: "model", 609 | showCheckbox: false, 610 | selectionLimit: -1, 611 | placeholder: "Select", 612 | groupBy: "", 613 | style: {}, 614 | emptyRecordMsg: "No Options Available", 615 | onSelect: () => {}, 616 | onRemove: () => {}, 617 | onKeyPressFn: () => {}, 618 | closeIcon: 'circle2', 619 | singleSelect: false, 620 | caseSensitiveSearch: false, 621 | id: '', 622 | name: '', 623 | closeOnSelect: true, 624 | avoidHighlightFirstOption: false, 625 | hidePlaceholder: false, 626 | showArrow: false, 627 | keepSearchTerm: false, 628 | customCloseIcon: '', 629 | className: '', 630 | customArrow: undefined, 631 | selectedValueDecorator: v => v, 632 | optionValueDecorator: v => v 633 | } as IMultiselectProps; 634 | --------------------------------------------------------------------------------