├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .stylelintrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── _config.yml ├── _data │ └── toc.yml ├── api.md ├── contribution.md ├── examples.md ├── getting-started.md ├── index.html ├── migration.md ├── pages │ ├── search.html │ └── sitemap.xml └── v1.md ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── CancellablePromise.ts ├── components │ ├── Main.svelte │ ├── OptionDropdown.svelte │ ├── OptionElement.svelte │ ├── OptionHolder.svelte │ ├── OptionList.svelte │ └── Tag.svelte ├── index.ts ├── types.ts └── utilities.ts ├── styles └── main.scss └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const isProd = process.env.BUILD === 'production'; 2 | 3 | const extentions = [ 4 | 'airbnb-base', 5 | 'eslint:recommended', 6 | 'plugin:eslint-comments/recommended', 7 | 'plugin:promise/recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | ]; 10 | 11 | if (isProd) { 12 | extentions.push('plugin:@typescript-eslint/recommended-requiring-type-checking'); 13 | } 14 | 15 | module.exports = { 16 | root: true, 17 | parser: '@typescript-eslint/parser', 18 | ignorePatterns: ['node_modules', 'docs', 'dist', 'nodesrc', 'index.html'], 19 | env: { 20 | browser: true, 21 | es6: true, 22 | node: true, 23 | }, 24 | parserOptions: { 25 | tsconfigRootDir: __dirname, 26 | project: ['./tsconfig.json'], 27 | extraFileExtensions: ['.svelte'], 28 | }, 29 | plugins: ['svelte3', '@typescript-eslint'], 30 | extends: extentions, 31 | rules: { 32 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 33 | 'no-shadow': 'off', 34 | '@typescript-eslint/no-shadow': 'error', 35 | }, 36 | overrides: [ 37 | { 38 | files: ['**/*.svelte'], 39 | processor: 'svelte3/svelte3', 40 | rules: { 41 | /* 42 | Disabling the following rules since they 43 | do not work as expected with svelte eslint plugin 44 | */ 45 | 'import/first': 'off', 46 | 'import/no-duplicates': 'off', 47 | 'import/no-mutable-exports': 'off', 48 | 'import/no-unresolved': 'off', 49 | 'import/extensions': 'off', 50 | 'import/prefer-default-export': 'off', 51 | '@typescript-eslint/no-unsafe-member-access': 'off', 52 | }, 53 | settings: { 54 | // eslint-disable-next-line global-require 55 | 'svelte3/typescript': require('typescript'), 56 | }, 57 | }, 58 | { 59 | files: ['**/*.js'], 60 | rules: { 61 | '@typescript-eslint/no-unsafe-assignment': 'off', 62 | '@typescript-eslint/no-unsafe-member-access': 'off', 63 | }, 64 | }, 65 | { 66 | files: ['**/*.ts'], 67 | rules: { 68 | 'import/extensions': [ 69 | 'error', 70 | 'ignorePackages', 71 | { 72 | js: 'never', 73 | ts: 'never', 74 | }, 75 | ], 76 | }, 77 | }, 78 | ], 79 | settings: { 80 | // eslint-disable-next-line global-require 81 | 'svelte3/typescript': require('typescript'), 82 | 'import/resolver': { 83 | node: { 84 | extensions: ['.js', '.ts'], 85 | }, 86 | }, 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x, 15.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm audit 30 | - run: npm run lint 31 | - run: npm run lint:style 32 | - run: npm run build 33 | - run: npm test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | nodesrc 4 | dist -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "plugins": [ 4 | "stylelint-scss" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v2.2.0 - In progress 2 | * Docs: examples 3 | * Docs: v2 API 4 | * Docs: Migration guide 5 | 6 | v2.1.0 7 | * Arrow, Loader position style fixes 8 | * Handled disabled options 9 | 10 | v2.0.0 11 | * Typescript support 12 | * Eslint configuration 13 | * Github actions 14 | * Animation support 15 | * Dropdown moved to body to avoid overlap issues 16 | * Accessibility improvements 17 | * Improved styling and basic layout moved as inline styles 18 | 19 | v1.1.0 20 | * Docs site 21 | * Ability to import as Svelte component 22 | * Bug fixes - missing icons, selection upon up/down arrow keys 23 | * CSS fixes 24 | * Added release assets, links to jsDelivr and unpkg 25 | 26 | v1.0.0 27 | * Basic options for search, multi-select and async-load 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Pavish Kumar Ramani Gopal 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 | # SelectMadu 2 | 3 | 4 | npm version 5 | 6 | 7 | 8 | license 9 | 10 | 11 |

12 | 13 | SelectMadu is a replacement for the select menu, with support for searching, multiple selections, async data loading and more. 14 | 15 | To get started, checkout documentation and examples at https://pavishkrg.com/select-madu 16 | 17 | 18 | ## Features 19 | * Searching through options. 20 | * Multi-select interface with tags. 21 | * Async data loading. 22 | * Dynamic state changes. 23 | * Multi-level nesting of options as groups. 24 | * Support for custom animations. 25 | * Pass custom option components. 26 | * Easily themable. 27 | * Better Accessibility. 28 | 29 | 30 | ## Installation 31 | 32 | ### Manual 33 | Download or source the javascript file and css file from [unpkg][unpkg-url] 34 | 35 | You can also download it directly from the [releases listed in select-madu repository][release-url] 36 | 37 | ```html 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | ### NPM 45 | You can install SelectMadu from npm using the following command. 46 | 47 | ```bash 48 | npm install select-madu --save 49 | ``` 50 | and then import it in your javascript file 51 | ```javascript 52 | import SelectMadu from 'select-madu'; 53 | ``` 54 | Note: CSS file has to be included separately 55 | 56 | ### Basic usage 57 | ```javascript 58 | import SelectMadu from 'select-madu'; 59 | 60 | //To create 61 | let instance = new SelectMadu({ 62 | //SelectMadu dom elements will be rendered within the specified parent element in target. 63 | target: document.querySelector("#parentElement"), 64 | 65 | //Properties for initializing SelectMadu. Refer Properties info below. 66 | props: { 67 | datasource: [ 68 | { text: "Ferrai" }, 69 | { text: "Lamborghini" }, 70 | { text: "Aston Martin" } 71 | ] 72 | } 73 | }); 74 | 75 | //To destroy and remove 76 | instance.$destroy(); 77 | ``` 78 | 79 | ### Svelte 80 | Optionally, if you are using Svelte you can import SelectMadu as a component. 81 | 82 | ```javascript 83 | import SelectMadu from 'select-madu'; 84 | 85 | 86 | ``` 87 | Note: CSS file has to be included separately 88 | 89 | Check out [Select madu home page][select-madu-url] for usage documentation and examples. 90 | 91 | 92 | ## API and Documentation 93 | Check out [SelectMadu home page][select-madu-url] for detailed documentation with properties information and API methods. 94 | 95 | 96 | ## Contribute 97 | Pull requests are encouraged and always welcome. Help make SelectMadu better! 98 | 99 | To work on SelectMadu 100 | ```bash 101 | git clone https://github.com/pavish/select-madu.git 102 | cd select-madu 103 | npm install 104 | ``` 105 | 106 | The component is written in [Svelte][svelte-url]. 107 | It's an awesome library, you should check it out! 108 | 109 | SCSS is used for styling. It is separate from the component and is within styles folder. 110 | 111 | To build, run 112 | ```bash 113 | npm run build 114 | ``` 115 | 116 | Build generates the build folder, which contains the iife variant, with and without minimization. 117 | It also generates the nodesrc folder which contains the esm and umd variants for importing the component within your app. 118 | 119 | You can also contribute by writing additional tests, adding issues that you encounter, or by helping with the documentation. 120 | 121 | 122 | ## License 123 | [MIT](LICENSE) 124 | 125 | [bundle-folder-url]: https://github.com/pavish/select-madu/tree/master/bundle 126 | [docs-folder-url]: https://github.com/pavish/select-madu/tree/master/docs 127 | [svelte-url]: https://svelte.dev/ 128 | [select-madu-url]: https://pavishkrg.com/select-madu 129 | 130 | [unpkg-url]: https://unpkg.com/browse/select-madu/dist/ 131 | [release-url]: https://github.com/pavish/select-madu/releases 132 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pavish/docsy-jekyll 2 | 3 | email: pavishkumar.r@gmail.com 4 | twitter: pavishkumar_r 5 | author: Pavish Kumar Ramani Gopal 6 | repo: "https://github.com/pavish/select-madu" 7 | repo_docs_folder: "docs" 8 | github_user: "pavish" 9 | github_repo: "select-madu" 10 | 11 | logo_pixels: 34 12 | color: "#3d4251" 13 | -------------------------------------------------------------------------------- /docs/_data/toc.yml: -------------------------------------------------------------------------------- 1 | - title: "About" 2 | url: "index" 3 | - title: "Getting Started" 4 | url: "getting-started" 5 | links: 6 | - title: Installation 7 | url: "getting-started#installation" 8 | - title: Basic Usage 9 | url: "getting-started#basic-usage" 10 | - title: "Examples" 11 | url: "examples" 12 | links: 13 | - title: Simple 14 | url: "examples#simple-selection" 15 | - title: Search 16 | url: "examples#search-custom-keys" 17 | - title: Multiple selection 18 | url: "examples#multiple-selection" 19 | - title: Nested options 20 | url: "examples#nested-options" 21 | - title: Remote(async) data 22 | url: "examples#remote-async-data" 23 | - title: Disabling component 24 | url: "examples#disabling-component" 25 | - title: Disabling options 26 | url: "examples#disabling-individual-options" 27 | - title: Animations 28 | url: "examples#animations" 29 | - title: Custom components 30 | url: "examples#custom-components" 31 | - title: "API" 32 | url: "api" 33 | links: 34 | - title: Properties 35 | url: "api#properties" 36 | - title: Methods 37 | url: "api#methods" 38 | - title: "Migration Guide" 39 | url: "migration" 40 | - title: "v1.x" 41 | url: "v1" 42 | - title: Contribution 43 | url: "contribution" 44 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | {% include alert.html type="warning" title="Still working on this page" content="This page is not yet ready. Help out by working on the documentaion, by clicking on edit this page link at the bottom of the page." %} 2 | -------------------------------------------------------------------------------- /docs/contribution.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SelectMadu - Contribution 3 | permalink: /contribution 4 | --- 5 | 6 | # Contribution 7 | Pull requests are encouraged and always welcome. Help make SelectMadu better! 8 | 9 | To work on SelectMadu 10 | ```bash 11 | git clone https://github.com/pavish/select-madu.git 12 | cd select-madu 13 | npm install 14 | ``` 15 | 16 | The component is written in [Svelte][svelte-url]. 17 | It's an awesome library, you should check it out! 18 | 19 | SCSS is used for styling. It is separate from the component and is within styles folder. 20 | 21 | To build, run 22 | ```bash 23 | npm run build 24 | ``` 25 | 26 | Build generates the nodesrc folder which contains the es and umd variants for importing using npm. 27 | Dist folder is generated containing the iife variant with and without minimization. 28 | CSS is generated from SCSS and is present within the dist folder. 29 | 30 | You can also contribute by writing additional tests, adding issues that you encounter, or by helping with the documentation. 31 | 32 | [svelte-url]: https://svelte.dev/ 33 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: SelectMadu - Examples 4 | permalink: /examples 5 | --- 6 | 7 | 8 | 9 | 14 | 15 | # Examples 16 | 17 | ### Simple selection 18 | 19 |
20 | 21 | ```javascript 22 | new SelectMadu({ 23 | target: document.querySelector("#example1"), 24 | props: { 25 | search: false, 26 | datasource: [ 27 | { text: "Option 1" }, 28 | { text: "Option 2" }, 29 | { text: "Option 3" } 30 | ] 31 | } 32 | }); 33 | ``` 34 | 35 | ```html 36 | 37 | 38 | ``` 39 | 40 | 53 | 54 | --- 55 | 56 | ### Search, Custom keys 57 | 58 |
59 | 60 | ```javascript 61 | new SelectMadu({ 62 | target: document.querySelector("#example2"), 63 | props: { 64 | search: true, //Optional, true is the default value for search 65 | textKey: "pokemon", 66 | valueKey: "id", 67 | datasource: [ 68 | { pokemon: "Pikachu", id: 1 }, 69 | { pokemon: "Bulbasaur", id: 2 }, 70 | { pokemon: "Charmander", id: 3 }, 71 | { pokemon: "Squirtle", id: 4 }, 72 | { pokemon: "Chikorita", id: 5 }, 73 | { pokemon: "Treecko", id: 6 } 74 | ] 75 | } 76 | }); 77 | ``` 78 | 79 | ```html 80 | 81 | 82 | ``` 83 | 84 | 102 | 103 | --- 104 | 105 | ### Multiple selection 106 | 107 |
108 | 109 | ```javascript 110 | new SelectMadu({ 111 | target: document.querySelector("#example3"), 112 | props: { 113 | multiple: true, 114 | datasource: [ 115 | { text: "Attack On Titan" }, 116 | { text: "Black Clover" }, 117 | { text: "My Hero Academia" }, 118 | { text: "Naruto" }, 119 | { text: "One Piece" }, 120 | { text: "Dragon Ball Z" }, 121 | { text: "Bleach" }, 122 | { text: "Fairy Tail" }, 123 | ] 124 | } 125 | }); 126 | ``` 127 | 128 | ```html 129 | 130 | 131 | ``` 132 | 133 | 151 | 152 | --- 153 | 154 | ### Nested options 155 | 156 |
157 | 158 | ```javascript 159 | new SelectMadu({ 160 | target: document.querySelector("#example4"), 161 | props: { 162 | datasource: [ 163 | { text: "1 Leaf" }, 164 | { 165 | text: "2 Parent", 166 | children: [ 167 | { text: "2.1 Leaf" }, 168 | { 169 | text: "2.2 Child", 170 | children: [ 171 | { text: "2.1.1 Leaf" }, 172 | { text: "2.1.2 Leaf" }, 173 | { text: "2.1.3 Leaf" } 174 | ] 175 | }, 176 | { text: "2.3 Leaf" } 177 | ] 178 | }, 179 | { 180 | text: "3 Parent", 181 | children: [ 182 | { text: "3.1 Leaf" } 183 | ] 184 | } 185 | ] 186 | } 187 | }); 188 | ``` 189 | 190 | ```html 191 | 192 | 193 | ``` 194 | 195 | 226 | 227 | --- 228 | 229 | ### Remote: async data 230 | 231 |
232 | 233 | ```javascript 234 | // Returns a promise, ideal for fetch calls to load data from server 235 | // Gets called on component initialization and on each keydown during search 236 | const asyncFn = (searchValue) => { 237 | return new Promise((resolve, reject) => { 238 | setTimeout(() => { 239 | let result = [ 240 | "Ooty, India", 241 | "Santiago, Chile", 242 | "Christchurch, New Zealand", 243 | "New York, USA" 244 | ]; 245 | 246 | if (searchValue && searchValue.trim()) { 247 | result = result.filter(elem => 248 | elem.toLowerCase().indexOf(searchValue.trim().toLowerCase()) > -1 249 | ); 250 | } 251 | 252 | resolve(result.map(entry => ({ text: entry }))); 253 | }, 2000); 254 | }); 255 | }; 256 | 257 | new SelectMadu({ 258 | target: document.querySelector("#example5"), 259 | props: { 260 | datasource: asyncFn 261 | } 262 | }); 263 | ``` 264 | 265 | ```html 266 | 267 | 268 | ``` 269 | 270 | 299 | 300 | --- 301 | 302 | ### Disabling component 303 | 304 |
305 | 306 | ```javascript 307 | new SelectMadu({ 308 | target: document.querySelector("#example6"), 309 | props: { 310 | disabled: true, 311 | datasource: [ 312 | { text: "Option 1" }, 313 | { text: "Option 2" }, 314 | { text: "Option 3" } 315 | ] 316 | } 317 | }); 318 | ``` 319 | 320 | ```html 321 | 322 | 323 | ``` 324 | 325 | 338 | 339 | --- 340 | 341 | ### Disabling individual options 342 | 343 |
344 | 345 | ```javascript 346 | new SelectMadu({ 347 | target: document.querySelector("#example7"), 348 | props: { 349 | datasource: [ 350 | { text: "Option 1" }, 351 | { text: "Option 2", disabled: true }, 352 | { text: "Option 3" } 353 | ] 354 | } 355 | }); 356 | ``` 357 | 358 | ```html 359 | 360 | 361 | ``` 362 | 363 | 375 | 376 | --- 377 | 378 | ### Animations 379 | 380 |
381 | 382 | ```javascript 383 | new SelectMadu({ 384 | target: document.querySelector("#example8"), 385 | props: { 386 | animate: true, 387 | datasource: [ 388 | { text: "Option 1" }, 389 | { text: "Option 2" }, 390 | { text: "Option 3" } 391 | ] 392 | } 393 | }); 394 | ``` 395 | 396 | ```html 397 | 398 | 399 | ``` 400 | 401 | 414 | 415 | --- 416 | 417 | ### Custom components 418 | 419 | {% include alert.html type="warning" title="Documentation work in progress." %} 420 | 421 |
422 | 423 | ```javascript 424 | import IconListComponent from './IconListComponent'; 425 | 426 | new SelectMadu({ 427 | target: document.querySelector("#example9"), 428 | props: { 429 | datasource: [ 430 | { text: "Option 1", optionComponent: IconListComponent }, 431 | { text: "Option 2" }, 432 | { text: "Option 3" } 433 | ] 434 | } 435 | }); 436 | ``` 437 | 438 | ```html 439 | 440 | 441 | ``` 442 | 443 | --- 444 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Select Madu - Getting started 3 | permalink: /getting-started 4 | --- 5 | 6 | # Getting started 7 | 8 | ## Installation 9 | 10 | ### Manual 11 | Download or source the javascript file and css file from [unpkg][unpkg-url] 12 | 13 | You can also download it directly from the [releases listed in select-madu repository][release-url] 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | ``` 21 | 22 | ### NPM 23 | You can install SelectMadu from npm using the following command. 24 | 25 | ```bash 26 | npm install select-madu --save 27 | ``` 28 | and then import it in your javascript file 29 | ```javascript 30 | import SelectMadu from 'select-madu'; 31 | ``` 32 | 33 | Note: CSS file has to be included separately 34 | 35 | ### Basic usage 36 | ```javascript 37 | import SelectMadu from 'select-madu'; 38 | 39 | //To create 40 | let instance = new SelectMadu({ 41 | //SelectMadu dom elements will be rendered within the specified parent element in target. 42 | target: document.querySelector("#parentElement"), 43 | 44 | //Properties for initializing SelectMadu. Refer Properties info below. 45 | props: { 46 | datasource: [ 47 | { text: "Ferrai" }, 48 | { text: "Lamborghini" }, 49 | { text: "Aston Martin" } 50 | ] 51 | } 52 | }); 53 | 54 | //To destroy and remove 55 | instance.$destroy(); 56 | ``` 57 | 58 | ### Svelte 59 | Optionally, if you are using Svelte you can import SelectMadu as a component. 60 | 61 | ```javascript 62 | import SelectMadu from 'select-madu'; 63 | 64 | 65 | ``` 66 | 67 | Note: CSS file has to be included separately 68 | 69 | ## Read more 70 | * Check out the properties and methods available in the [API documentation page][api-document-page-url] 71 | * Find usage examples in the [examples page][example-page-url] 72 | 73 | [example-page-url]: {{ site.baseurl }}/examples 74 | [api-document-page-url]: {{ site.baseurl }}/api 75 | 76 | [unpkg-url]: https://unpkg.com/browse/select-madu/dist/ 77 | [release-url]: https://github.com/pavish/select-madu/releases 78 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: SelectMadu 4 | permalink: / 5 | --- 6 | 7 |

SelectMadu

8 |

SelectMadu is a replacement for the select menu, with support for searching, multiple selections, async data loading and more.

9 | 10 |

Features

11 |
    12 |
  • Searching through options.
  • 13 |
  • Multi-select interface with tags.
  • 14 |
  • Async data loading.
  • 15 |
  • Dynamic state changes.
  • 16 |
  • Multi-level nesting of options as groups.
  • 17 |
  • Support for custom animations.
  • 18 |
  • Pass custom option components.
  • 19 |
  • Easily themable.
  • 20 |
  • Better Accessibility.
  • 21 |
22 | 23 |

Have a look

24 |

Select a car brand for no reason

25 |
26 | 27 |

Get started

28 |

Check out the following links to get started with SelectMadu

29 | 34 | 35 |

License

36 |

SelectMadu is licensed under the MIT License

37 | 38 | 39 | 40 | 41 | 57 | 58 | 63 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | {% include alert.html type="warning" title="Still working on this page" content="This page is not yet ready. Help out by working on the documentaion, by clicking on edit this page link at the bottom of the page." %} 2 | -------------------------------------------------------------------------------- /docs/pages/search.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Search 4 | sitemap: false 5 | permalink: /search/ 6 | not_editable: true 7 | excluded_in_search: true 8 | --- 9 | {% include search.html %} 10 | -------------------------------------------------------------------------------- /docs/pages/sitemap.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | permalink: /sitemap.xml 4 | --- 5 | {% include sitemap.xml %} 6 | -------------------------------------------------------------------------------- /docs/v1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Select Madu - API 3 | permalink: /v1 4 | --- 5 | 6 | # v1.x API 7 | 8 | ## Properties 9 | SelectMadu can be initialized with the following properties 10 | 11 | | Property | Required | Default Value | Description | 12 | | :----------------: | :------: | :-----------------------: | :-------------------------------------------------------------------: | 13 | | `datasource` | ✔ | `[]` | Array or a function returning a Promise object for async loading | 14 | | `selected` | | First value of datasource or undefined if not present | Object or array of selected objects | 15 | | `multiple` | | `false` | If true, then multi selection will be used | 16 | | `search` | | `true` | For enabling/disabling searching | 17 | | `textKey` | | `"text"` | The key to use to display in the datasource result | 18 | | `valueKey` | | `"text"` | The key to use to identify the value of the results in the datasource. | 19 | | `childKey` | | `"children"` | The key to use to obtain the values of the nested optgroup | 20 | | `disabled` | | `false` | If true, the instance will be disabled | 21 | 22 | ## Methods 23 | The following methods can be called from the component instance 24 | 25 | | Method | Description | Usage | 26 | | :----------------: | :------------------------------------------------------------: | :--------: | 27 | | `getSelected` | Returns the current selected value or values | `instance.getSelected()` 28 | | `$set` | Use this method to set any of the properties dynamically. | `instance.$set('disabled', true)` 29 | | `$destroy` | Destroy the SelectMadu instance | `instance.$destroy()` 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 |
15 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "select-madu", 3 | "consName": "SelectMadu", 4 | "version": "2.2.0", 5 | "description": "Select-madu is a replacement for the select menu, with support for searching, multiple selections, async loading, animations and more.", 6 | "main": "nodesrc/index.js", 7 | "module": "nodesrc/index.mjs", 8 | "svelte": "nodesrc/index.js", 9 | "scripts": { 10 | "build:css": "sass --no-source-map styles/main.scss dist/selectmadu.css", 11 | "bundle": "cross-env BUILD=production rollup -c", 12 | "build": "npm run bundle && npm run build:css", 13 | "lint": "cross-env BUILD=production eslint .", 14 | "lint:style": "npx stylelint \"styles/*.scss\"", 15 | "test": "echo \"No test specified\" && exit 0", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/pavish/select-madu.git" 21 | }, 22 | "keywords": [ 23 | "select", 24 | "selectmenu", 25 | "svelte", 26 | "autocomplete", 27 | "dropdown", 28 | "multiselect", 29 | "tag", 30 | "tagging", 31 | "typeahead" 32 | ], 33 | "author": "Pavish Kumar Ramani Gopal ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/pavish/select-madu/issues" 37 | }, 38 | "homepage": "https://github.com/pavish/select-madu#readme", 39 | "devDependencies": { 40 | "@rollup/plugin-node-resolve": "^9.0.0", 41 | "@rollup/plugin-typescript": "^6.1.0", 42 | "@tsconfig/svelte": "^1.0.10", 43 | "@typescript-eslint/eslint-plugin": "^4.15.2", 44 | "@typescript-eslint/parser": "^4.15.2", 45 | "cross-env": "^7.0.3", 46 | "eslint": "^7.20.0", 47 | "eslint-config-airbnb-base": "^14.2.1", 48 | "eslint-plugin-eslint-comments": "^3.2.0", 49 | "eslint-plugin-import": "^2.22.1", 50 | "eslint-plugin-promise": "^4.3.1", 51 | "eslint-plugin-svelte3": "^3.1.1", 52 | "npm-run-all": "^4.1.5", 53 | "rollup": "^2.39.1", 54 | "rollup-plugin-svelte": "^6.1.1", 55 | "rollup-plugin-terser": "^7.0.2", 56 | "sass": "^1.32.8", 57 | "stylelint": "^13.12.0", 58 | "stylelint-config-standard": "^21.0.0", 59 | "stylelint-scss": "^3.19.0", 60 | "svelte": "^3.32.3", 61 | "svelte-check": "^1.1.35", 62 | "svelte-preprocess": "^4.6.9", 63 | "tslib": "^2.1.0", 64 | "typescript": "^4.0.6" 65 | }, 66 | "files": [ 67 | "src", 68 | "styles", 69 | "nodesrc", 70 | "dist" 71 | ], 72 | "dependencies": { 73 | "@popperjs/core": "^2.9.1" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import preprocess from 'svelte-preprocess'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import pkg from './package.json'; 7 | 8 | const isProd = process.env.BUILD === 'production'; 9 | const name = pkg.consName; 10 | 11 | export default { 12 | input: 'src/index.ts', 13 | output: [ 14 | { file: pkg.module, format: 'es', name }, 15 | { file: pkg.main, format: 'umd', name }, 16 | { 17 | file: 'dist/selectmadu.min.js', 18 | format: 'iife', 19 | name, 20 | sourcemap: !isProd, 21 | plugins: [ 22 | terser({ 23 | format: { 24 | comments: false, 25 | }, 26 | }), 27 | ], 28 | }, 29 | ], 30 | plugins: [ 31 | svelte({ 32 | dev: !isProd, 33 | emitCss: false, 34 | legacy: true, 35 | preprocess: preprocess(), 36 | }), 37 | typescript({ sourceMap: !isProd }), 38 | resolve(), 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /src/CancellablePromise.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CancellablePromiseExecutor, 3 | CancellablePromiseLike, 4 | } from './types'; 5 | 6 | export default class CancellablePromise implements CancellablePromiseLike { 7 | #isActive: boolean; 8 | 9 | #promise: Promise; 10 | 11 | #onCancel: () => void; 12 | 13 | #onCatch: (err: unknown) => void; 14 | 15 | constructor(fn: CancellablePromiseExecutor, onCancel?: () => void) { 16 | this.#promise = new Promise(fn); 17 | this.#isActive = true; 18 | this.#onCancel = onCancel; 19 | return this; 20 | } 21 | 22 | cancel(): void { 23 | this.#isActive = false; 24 | if (typeof this.#onCancel === 'function') { 25 | this.#onCancel(); 26 | } 27 | } 28 | 29 | catch(catcher: (error: unknown) => void): void { 30 | this.#onCatch = catcher; 31 | } 32 | 33 | then( 34 | resolve?: (value: T) => void, 35 | reject?: (reason?: unknown) => void, 36 | ): CancellablePromiseLike { 37 | // eslint-disable-next-line @typescript-eslint/no-this-alias 38 | const self = this; 39 | this.#promise.then( 40 | (result) => { 41 | if (self.#isActive && resolve) { 42 | resolve(result); 43 | } 44 | return result; 45 | }, 46 | (err) => { 47 | if (self.#isActive && reject) { 48 | reject(err); 49 | } 50 | }, 51 | ).catch((err) => { 52 | self.#onCatch(err); 53 | }); 54 | return this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Main.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 327 | 328 | 329 | 330 |
340 | 341 |
342 | {#if multiple && Array.isArray(selected) && selected.length > 0} 343 |
    345 | {#each selected as elem, index (elem[keys.value])} 346 | 348 | {/each} 349 |
350 | {/if} 351 | 352 | {#if search && isOpen} 353 | 362 | 363 | {:else if isPlaceHolder} 364 | {placeholder} 365 | 366 | {:else if !multiple} 367 | 369 | {selected[keys.text]} 370 | 371 | 372 | {/if} 373 |
374 | 375 | 390 |
391 | 392 | {#if isOpen} 393 | 399 | {/if} 400 | -------------------------------------------------------------------------------- /src/components/OptionDropdown.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 117 | -------------------------------------------------------------------------------- /src/components/OptionElement.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51 | 52 | {#if option[keys.child]} 53 |
  • 55 | {option[keys.text]} 56 | 57 |
      58 | 59 |
    60 |
  • 61 | 62 | {:else} 63 |
  • 71 | 72 | {#if optionComponent} 73 | 75 | 76 | {:else} 77 | {option[keys.text]} 78 | 79 | {/if} 80 |
  • 81 | {/if} 82 | -------------------------------------------------------------------------------- /src/components/OptionHolder.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 131 | 132 |
    135 | 136 | {#if isOpen} 137 |
    142 |
      145 | {#if options.length > 0} 146 | 148 | {:else} 149 | 156 | {/if} 157 |
    158 |
    159 | {/if} 160 | 161 |
    162 | -------------------------------------------------------------------------------- /src/components/OptionList.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | {#each options as option} 23 | 26 | 29 | 30 | {/each} 31 | -------------------------------------------------------------------------------- /src/components/Tag.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 |
  • 29 | {title} 30 | 31 | 41 |
  • 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './components/Main.svelte'; 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import type { TransitionConfig } from 'svelte/types/runtime/transition'; 3 | 4 | export interface Option { 5 | classes?: string | string[] 6 | disabled?: boolean 7 | [propName: string]: string | string[] | boolean | Option[] 8 | } 9 | 10 | export interface Formatter { 11 | [propName: string]: (opt: Option) => string 12 | } 13 | 14 | export interface Keys { 15 | text: string 16 | value: string 17 | child: string 18 | } 19 | 20 | export enum States { 21 | Loading = 'Loading', 22 | Ready = 'Ready', 23 | Error = 'Error' 24 | } 25 | 26 | export interface CancellablePromiseLike { 27 | then: ( 28 | resolve?: (value: T) => void, 29 | reject?: (reason?: unknown) => void 30 | ) => CancellablePromiseLike 31 | catch: ( 32 | catcher?: (reason?: unknown) => void 33 | ) => void 34 | cancel: () => void 35 | } 36 | 37 | export type CancellablePromiseExecutor = ( 38 | resolve: (value: T | PromiseLike) => void, 39 | reject?: (reason?: Error) => void 40 | ) => void 41 | 42 | export type Selected = Option | Option[] | undefined; 43 | export type DataSource = Option[] 44 | | ((searchValue?: string) => Option[]) 45 | | ((searchValue?: string) => Promise) 46 | | ((searchValue?: string) => CancellablePromiseLike); 47 | 48 | export interface AnimationParams { 49 | delay?: number 50 | duration?: number 51 | } 52 | 53 | export type TransitionFunction = (node: Element, params?: AnimationParams) => TransitionConfig; 54 | 55 | export type Animation = boolean | { 56 | duration?: number 57 | delay?: number 58 | transitionFn?: TransitionFunction; 59 | }; 60 | 61 | export interface DropdownKeyboardInteractions { 62 | selectHovered: () => void 63 | moveDown: () => void 64 | moveUp: () => void 65 | } 66 | 67 | export interface PopperModifierFunctionParam { 68 | state: { 69 | styles: { 70 | popper: { 71 | minWidth: string 72 | } 73 | }, 74 | rects: { 75 | reference: { 76 | width: string 77 | } 78 | }, 79 | elements: { 80 | popper: { 81 | style: { 82 | minWidth: string 83 | } 84 | }, 85 | reference: { 86 | offsetWidth: string 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import type { TransitionConfig } from 'svelte/types/runtime/transition'; 3 | import type { 4 | DataSource, 5 | Keys, 6 | Option, 7 | Animation, 8 | AnimationParams, 9 | TransitionFunction, 10 | PopperModifierFunctionParam, 11 | } from './types'; 12 | import CancellablePromise from './CancellablePromise'; 13 | 14 | export const isOutOfBounds = ( 15 | elem: HTMLElement, 16 | refElem: HTMLElement, 17 | componentId: number, 18 | ): boolean => { 19 | const subString = `select-madu-${componentId}`; 20 | let parentElem = elem; 21 | while (parentElem !== refElem) { 22 | if (parentElem.id && parentElem.id.indexOf(subString) > -1) { 23 | return false; 24 | } 25 | parentElem = parentElem.parentElement; 26 | if (!parentElem) { 27 | return true; 28 | } 29 | } 30 | return (parentElem !== refElem); 31 | }; 32 | 33 | export const offsetTop = (elem: HTMLElement, parentRefElem: HTMLElement): number => { 34 | let parentElem = elem.parentElement; 35 | let offset = elem.offsetTop; 36 | while (parentElem && parentElem !== parentRefElem) { 37 | offset += parentElem.offsetTop; 38 | parentElem = parentElem.parentElement; 39 | } 40 | return offset; 41 | }; 42 | 43 | const transform = ( 44 | node, { delay = 0, duration = 100, baseScale = 0.75 }, 45 | ): TransitionConfig => { 46 | const is = 1 - baseScale; 47 | 48 | return { 49 | delay, 50 | duration, 51 | css: (t: number) => { 52 | const translate = (t - 1) * 10; 53 | return `opacity: ${t}; transform-origin: 60% 0; 54 | transform: scale(${(t * is) + baseScale}) translateY(${translate}px); 55 | transition: all .1s cubic-bezier(0.5, 0, 0, 1.25), opacity .15s ease-out`; 56 | }, 57 | }; 58 | }; 59 | 60 | export const getAnimation = (_animate: Animation) : TransitionFunction => { 61 | if (typeof _animate === 'boolean') { 62 | return _animate ? transform : () => ({ delay: 0, duration: 0 }); 63 | } 64 | if (_animate.transitionFn) { 65 | return _animate.transitionFn; 66 | } 67 | return transform; 68 | }; 69 | 70 | export const getAnimationParams = (_animate: Animation): AnimationParams => { 71 | let delay = 0; 72 | let duration = 100; 73 | if (typeof _animate !== 'boolean') { 74 | delay = _animate.delay || 0; 75 | duration = typeof _animate.duration === 'number' 76 | ? _animate.duration 77 | : 100; 78 | } 79 | return { delay, duration }; 80 | }; 81 | 82 | export const sameWidthModifier = { 83 | name: 'sameWidth', 84 | enabled: true, 85 | phase: 'beforeWrite', 86 | requires: ['computeStyles'], 87 | fn: (obj: PopperModifierFunctionParam): void => { 88 | // eslint-disable-next-line no-param-reassign 89 | obj.state.styles.popper.minWidth = `${obj.state.rects.reference.width}px`; 90 | }, 91 | effect: (obj: PopperModifierFunctionParam): void => { 92 | // eslint-disable-next-line no-param-reassign 93 | obj.state.elements.popper.style.minWidth = `${ 94 | obj.state.elements.reference.offsetWidth 95 | }px`; 96 | }, 97 | }; 98 | 99 | function getHovered(scrollParentRef: HTMLElement): HTMLElement { 100 | let hoveredElem = scrollParentRef.querySelector('li.o.hovered:not(.disabled)'); 101 | if (!hoveredElem) { 102 | hoveredElem = scrollParentRef.querySelector('li.o:not(.disabled)'); 103 | } 104 | return hoveredElem as HTMLElement; 105 | } 106 | 107 | export const arrowDown = (scrollParentRef: HTMLElement): void => { 108 | const elem = getHovered(scrollParentRef); 109 | if (elem) { 110 | let nextElem: HTMLElement; 111 | 112 | if (elem.classList.contains('hovered')) { 113 | const nodeList = scrollParentRef.querySelectorAll('li.o:not(.disabled)'); 114 | if (nodeList.length > 0) { 115 | let index = 0; 116 | for (let i = 0; i < nodeList.length; i += 1) { 117 | if (nodeList[i].classList.contains('hovered')) { 118 | if (i < nodeList.length - 1) { 119 | index = i + 1; 120 | } 121 | break; 122 | } 123 | } 124 | nextElem = nodeList[index] as HTMLElement; 125 | } 126 | elem.classList.remove('hovered'); 127 | } else { 128 | nextElem = elem; 129 | } 130 | 131 | if (nextElem) { 132 | nextElem.classList.add('hovered'); 133 | } 134 | } 135 | }; 136 | 137 | export const arrowUp = (scrollParentRef: HTMLElement): void => { 138 | const elem = getHovered(scrollParentRef); 139 | if (elem) { 140 | let prevElem: HTMLElement; 141 | 142 | const nodeList = scrollParentRef.querySelectorAll('li.o:not(.disabled)'); 143 | if (nodeList.length > 0) { 144 | if (elem.classList.contains('hovered')) { 145 | let index = nodeList.length - 1; 146 | for (let i = nodeList.length - 1; i >= 0; i -= 1) { 147 | if (nodeList[i].classList.contains('hovered')) { 148 | if (i > 0) { 149 | index = i - 1; 150 | } 151 | break; 152 | } 153 | } 154 | prevElem = nodeList[index] as HTMLElement; 155 | elem.classList.remove('hovered'); 156 | } else { 157 | prevElem = nodeList[nodeList.length - 1] as HTMLElement; 158 | } 159 | } 160 | 161 | if (prevElem) { 162 | prevElem.classList.add('hovered'); 163 | } 164 | } 165 | }; 166 | 167 | export const chooseHovered = (scrollParentRef: HTMLElement): void => { 168 | const hoveredElem = scrollParentRef.querySelector('li.o.hovered:not(.disabled)'); 169 | if (hoveredElem) { 170 | // TODO: Change to custom event 171 | const helem = hoveredElem as HTMLElement; 172 | helem.click(); 173 | } else { 174 | arrowDown(scrollParentRef); 175 | } 176 | }; 177 | 178 | export const subTree = ( 179 | tree: Option[], 180 | childKey: string, 181 | conditionFn: ((element: Option) => boolean), 182 | ): Option[] => { 183 | const stree: Option[] = []; 184 | tree.forEach((elem) => { 185 | if (elem[childKey]) { 186 | const childSubTree = subTree(elem[childKey] as Option[], childKey, conditionFn); 187 | if (childSubTree.length > 0) { 188 | stree.push( 189 | { ...elem, [childKey]: childSubTree }, 190 | ); 191 | } 192 | } else if (conditionFn(elem)) { 193 | stree.push(elem); 194 | } 195 | }); 196 | return stree; 197 | }; 198 | 199 | export const stringContains = ( 200 | actualString: string, 201 | searchVal: string, 202 | ): boolean => actualString.toLowerCase().trim().indexOf(searchVal.toLowerCase().trim()) !== -1; 203 | 204 | export const filterTree = ( 205 | tree: Option[], 206 | keys: { child: string, text: string }, 207 | searchVal: string, 208 | ): Option[] => subTree(tree, keys.child, (el) => { 209 | const text = el[keys.text]; 210 | if (typeof text === 'string') { 211 | return stringContains(text, searchVal); 212 | } 213 | return false; 214 | }); 215 | 216 | export const fetchOptions = ( 217 | datasource: DataSource, 218 | searchVal: string, 219 | keys: Keys, 220 | ): CancellablePromise => { 221 | if (datasource) { 222 | if (typeof datasource === 'function') { 223 | const fetchResult = datasource(searchVal); 224 | if (Array.isArray(fetchResult)) { 225 | return new CancellablePromise((resolve) => { 226 | resolve(fetchResult); 227 | }); 228 | } 229 | if (typeof fetchResult.then === 'function') { 230 | return new CancellablePromise( 231 | (resolve, reject) => { 232 | fetchResult.then( 233 | (res: Option[]) => { 234 | resolve(res); 235 | return res; 236 | }, 237 | (err) => { 238 | reject(err); 239 | }, 240 | ).catch?.((err) => { 241 | reject(err); 242 | }); 243 | }, 244 | () => { 245 | if ('cancel' in fetchResult && typeof fetchResult.cancel === 'function') { 246 | fetchResult.cancel(); 247 | } 248 | }, 249 | ); 250 | } 251 | } else { 252 | const result = filterTree(datasource, keys, searchVal); 253 | return new CancellablePromise((resolve) => { 254 | resolve(result); 255 | }); 256 | } 257 | } 258 | 259 | return new CancellablePromise((_resolve, reject) => { 260 | reject(new Error('select-madu datasource is empty')); 261 | }); 262 | }; 263 | 264 | export const setAttribute = ( 265 | node: HTMLElement, 266 | parameters: { key: string, value?: string }, 267 | ): { 268 | destroy: () => void, 269 | update: ( 270 | parameters: { key: string, value?: string } 271 | ) => void, 272 | } => { 273 | if (parameters.value) { 274 | node.setAttribute(parameters.key, parameters.value); 275 | } else { 276 | node.removeAttribute(parameters.key); 277 | } 278 | 279 | return { 280 | update(params) { 281 | if (params.value) { 282 | node.setAttribute(params.key, params.value); 283 | } else { 284 | node.removeAttribute(params.key); 285 | } 286 | }, 287 | 288 | destroy() { 289 | node.removeAttribute(parameters.key); 290 | }, 291 | }; 292 | }; 293 | -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | $base-color: #adadad; 2 | $highlight-color: #339dff; 3 | $disabled-text-color: #979797; 4 | $disabled-text-dark-color: #777; 5 | 6 | .select-madu { 7 | vertical-align: middle; 8 | cursor: pointer; 9 | border-color: rgba($base-color, 0.4); 10 | border-radius: 5px; 11 | 12 | &.disabled { 13 | pointer-events: none; 14 | background: rgba($base-color, 0.1); 15 | color: $disabled-text-dark-color; 16 | } 17 | 18 | &-inner { 19 | padding: 9px 12px; 20 | padding-right: 32px; 21 | color: inherit; 22 | 23 | &-tag { 24 | background: rgba($base-color, 0.2); 25 | display: inline-block; 26 | border-radius: 3px; 27 | word-break: break-all; 28 | border: 1px solid rgba($base-color, 0.4); 29 | margin: 0 3px; 30 | padding: 0 0 0 5px; 31 | cursor: default; 32 | 33 | span { 34 | vertical-align: middle; 35 | } 36 | 37 | button.select-madu-icon { 38 | padding: 5px 2px; 39 | border: 0; 40 | border-left: 1px solid rgba($base-color, 0.4); 41 | display: inline-block; 42 | outline: 0; 43 | cursor: pointer; 44 | background: transparent; 45 | 46 | &:hover { 47 | background: rgba($base-color, 0.3); 48 | } 49 | 50 | &:focus { 51 | background: rgba($base-color, 0.4); 52 | } 53 | } 54 | } 55 | 56 | input[type='search'].select-madu-input { 57 | min-height: 18px; 58 | line-height: inherit; 59 | 60 | &::-webkit-search-decoration, 61 | &::-webkit-search-cancel-button, 62 | &::-webkit-search-results-button, 63 | &::-webkit-search-results-decoration { 64 | -webkit-appearance: none; 65 | } 66 | } 67 | } 68 | 69 | &-arrow { 70 | position: absolute; 71 | top: 0; 72 | right: 0; 73 | bottom: 0; 74 | padding: 11px; 75 | line-height: 0; 76 | } 77 | 78 | &-arrow &-spinner { 79 | width: 18px; 80 | height: 18px; 81 | position: relative; 82 | transform: translateY(-50%); 83 | top: 50%; 84 | 85 | .spinner-border { 86 | color: rgba($base-color, 0.8); 87 | position: relative; 88 | display: inline-block; 89 | width: calc(100% - 5px); 90 | height: calc(100% - 5px); 91 | vertical-align: text-bottom; 92 | border: 0.15em solid currentColor; 93 | border-right-color: transparent; 94 | border-radius: 50%; 95 | -webkit-animation: select-madu-spinner-border 0.75s linear infinite; 96 | animation: select-madu-spinner-border 0.75s linear infinite; 97 | } 98 | } 99 | 100 | &-arrow.loading { 101 | padding: 9px; 102 | } 103 | 104 | &-icon { 105 | display: inline-block; 106 | vertical-align: middle; 107 | 108 | >svg { 109 | stroke: currentColor; 110 | fill: none; 111 | stroke-width: 2; 112 | stroke-linecap: round; 113 | stroke-linejoin: round; 114 | } 115 | } 116 | 117 | &-arrow &-icon { 118 | transform: translateY(-50%); 119 | top: 50%; 120 | position: relative; 121 | line-height: 0px; 122 | } 123 | 124 | &.open &-arrow &-icon { 125 | transform: translateY(-50%) rotate(180deg); 126 | -webkit-transform: translateY(-50%) rotate(180deg); 127 | } 128 | 129 | &.animate &-arrow &-icon { 130 | transition: all 0.15s ease-in-out; 131 | -webkit-transition: all 0.15s ease-in-out; 132 | } 133 | 134 | &.placeholder &-inner { 135 | color: $disabled-text-color; 136 | } 137 | 138 | &.multiple:not(.placeholder) &-inner { 139 | padding: 5px 4px; 140 | height: auto; 141 | } 142 | 143 | &:focus, 144 | &.focus { 145 | outline: 0; 146 | border-color: $highlight-color; 147 | } 148 | 149 | &.open.search &-inner { 150 | cursor: text; 151 | } 152 | 153 | &-dropdown &-options { 154 | border-color: rgba($base-color, 0.4); 155 | border-radius: 5px; 156 | 157 | ul { 158 | li { 159 | padding: 9px 12px; 160 | } 161 | 162 | li.o { 163 | cursor: pointer; 164 | } 165 | 166 | li.o.selected { 167 | background: rgba($base-color, 0.2); 168 | color: inherit; 169 | } 170 | 171 | li.o:hover, 172 | li.o.hovered { 173 | background: $highlight-color; 174 | color: #fff; 175 | } 176 | 177 | li.o-h { 178 | padding: 0; 179 | 180 | strong { 181 | padding: 9px 12px; 182 | } 183 | } 184 | 185 | ul + li.o-h { 186 | margin-top: 5px; 187 | } 188 | 189 | >ul { 190 | li { 191 | padding: 9px 18px; 192 | } 193 | } 194 | 195 | li.disabled { 196 | pointer-events: none; 197 | background: rgba($base-color, 0.1); 198 | color: $disabled-text-dark-color; 199 | } 200 | } 201 | } 202 | 203 | &-dropdown &-options &-sub-text { 204 | color: $disabled-text-color; 205 | } 206 | } 207 | 208 | @keyframes select-madu-spinner-border { 209 | to { transform: rotate(360deg); } 210 | } 211 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/**/*", "**.js"], 5 | "exclude": ["node_modules/*", "dist/*", "docs/*", "nodesrc/*"] 6 | } 7 | --------------------------------------------------------------------------------