├── .github └── workflows │ └── codeql-analysis.yml ├── LICENSE ├── README.md ├── alpinejs-multiselect.gif ├── alpinejs-multiselect.js └── docs ├── _config.yml ├── index.html └── index.md /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '16 10 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alexander Pechkarev 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 | # alpinejs-multiselect 2 | Alpine.js MultiSelect component 3 | 4 | ## Features 5 | - Configurable pre-selected option 6 | - Fast search 7 | - Configurable searching values 8 | - Custom template 9 | 10 | ## Dependency 11 | - [Apline.js](https://alpinejs.dev/essentials/installation) 12 | 13 | ### [Demo](https://alexpechkarev.github.io/alpinejs-multiselect/) 14 | 15 | 16 | ![Alpine.js MultiSelect](alpinejs-multiselect.gif) 17 | 18 | ## Installation 19 | 20 | Install [Alpine.js](https://alpinejs.dev/essentials/installation). 21 | 22 | The example uses [Alpine's Focus plugin](https://alpinejs.dev/plugins/focus), this is optional. 23 | 24 | Specify your select element, `data-search` attribute value used to match against the search string, ignoring upper/lower case differences. 25 | 26 | ```html 27 | 33 | ``` 34 | 35 | Initiate the Apline.js component, pre-selected options can be defined by initializing `selected` property with an array of values. `elementId` references the select element `id` defined above. 36 | 37 | ```html 38 |
39 | ``` 40 | 41 | Add the Alpine component code into your application. 42 | 43 | ```javascript 44 | document.addEventListener("alpine:init", () => { 45 | Alpine.data("alpineMuliSelect", (obj) => ({ 46 | elementId: obj.elementId, 47 | options: [], 48 | selected: obj.selected, 49 | selectedElms: [], 50 | show: false, 51 | search: '', 52 | open() { 53 | this.show = true 54 | }, 55 | close() { 56 | this.show = false 57 | }, 58 | toggle() { 59 | this.show = !this.show 60 | }, 61 | isOpen() { 62 | return this.show === true 63 | }, 64 | 65 | // Initializing component 66 | init() { 67 | const options = document.getElementById(this.elementId).options; 68 | for (let i = 0; i < options.length; i++) { 69 | 70 | this.options.push({ 71 | value: options[i].value, 72 | text: options[i].innerText, 73 | search: options[i].dataset.search, 74 | selected: Object.values(this.selected).includes(options[i].value) 75 | }); 76 | 77 | if (this.options[i].selected) { 78 | this.selectedElms.push(this.options[i]) 79 | } 80 | } 81 | 82 | // searching for the given value 83 | this.$watch("search", (e => { 84 | this.options = [] 85 | const options = document.getElementById(this.elementId).options; 86 | Object.values(options).filter((el) => { 87 | var reg = new RegExp(this.search, 'gi'); 88 | return el.dataset.search.match(reg) 89 | }).forEach((el) => { 90 | let newel = { 91 | value: el.value, 92 | text: el.innerText, 93 | search: el.dataset.search, 94 | selected: Object.values(this.selected).includes(el.value) 95 | } 96 | this.options.push(newel); 97 | }) 98 | })); 99 | }, 100 | // clear search field 101 | clear() { 102 | this.search = '' 103 | }, 104 | // deselect selected options 105 | deselect() { 106 | setTimeout(() => { 107 | this.selected = [] 108 | this.selectedElms = [] 109 | Object.keys(this.options).forEach((key) => { 110 | this.options[key].selected = false; 111 | }) 112 | }, 100) 113 | }, 114 | // select given option 115 | select(index, event) { 116 | if (!this.options[index].selected) { 117 | this.options[index].selected = true; 118 | this.options[index].element = event.target; 119 | this.selected.push(this.options[index].value); 120 | this.selectedElms.push(this.options[index]); 121 | 122 | } else { 123 | this.selected.splice(this.selected.lastIndexOf(index), 1); 124 | this.options[index].selected = false 125 | Object.keys(this.selectedElms).forEach((key) => { 126 | if (this.selectedElms[key].value == this.options[index].value) { 127 | setTimeout(() => { 128 | this.selectedElms.splice(key, 1) 129 | }, 100) 130 | } 131 | }) 132 | } 133 | }, 134 | // remove from selected option 135 | remove(index, option) { 136 | this.selectedElms.splice(index, 1); 137 | Object.keys(this.selected).forEach((skey) => { 138 | if (this.selected[skey] == option.value) { 139 | this.selected.splice(skey, 1); 140 | } 141 | }); 142 | Object.keys(this.options).forEach((key) => { 143 | if (this.options[key].value == option.value) { 144 | this.options[key].selected = false; 145 | } 146 | }); 147 | }, 148 | // filter out selected elements 149 | selectedElements() { 150 | return this.options.filter(op => op.selected === true) 151 | }, 152 | // get selected values 153 | selectedValues() { 154 | return this.options.filter(op => op.selected === true).map(el => el.value) 155 | } 156 | })); 157 | }); 158 | ``` 159 | 160 | ## Support 161 | ------- 162 | [Please open an issue on GitHub](https://github.com/alexpechkarev/alpinejs-multiselect/issues) 163 | 164 | 165 | ## Licence 166 | ------- 167 | Released under the MIT Licence. See the bundled 168 | [LICENCE](https://github.com/alexpechkarev/alpinejs-multiselect/blob/main/LICENSE) 169 | file for details. 170 | -------------------------------------------------------------------------------- /alpinejs-multiselect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexpechkarev/alpinejs-multiselect/f74e543d9331fa395836a40b69abb35ee129bf8e/alpinejs-multiselect.gif -------------------------------------------------------------------------------- /alpinejs-multiselect.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("alpine:init", () => { 2 | Alpine.data("alpineMuliSelect", (obj) => ({ 3 | elementId: obj.elementId, 4 | options: [], 5 | selected: obj.selected, 6 | selectedElms: [], 7 | show: false, 8 | search: '', 9 | open() { 10 | this.show = true 11 | }, 12 | close() { 13 | this.show = false 14 | }, 15 | toggle() { 16 | this.show = !this.show 17 | }, 18 | isOpen() { 19 | return this.show === true 20 | }, 21 | 22 | // Initializing component 23 | init() { 24 | const options = document.getElementById(this.elementId).options; 25 | for (let i = 0; i < options.length; i++) { 26 | 27 | this.options.push({ 28 | value: options[i].value, 29 | text: options[i].innerText, 30 | search: options[i].dataset.search, 31 | selected: Object.values(this.selected).includes(options[i].value) 32 | }); 33 | 34 | if (this.options[i].selected) { 35 | this.selectedElms.push(this.options[i]) 36 | } 37 | } 38 | 39 | // searching for the given value 40 | this.$watch("search", (e => { 41 | this.options = [] 42 | const options = document.getElementById(this.elementId).options; 43 | Object.values(options).filter((el) => { 44 | var reg = new RegExp(this.search, 'gi'); 45 | return el.dataset.search.match(reg) 46 | }).forEach((el) => { 47 | let newel = { 48 | value: el.value, 49 | text: el.innerText, 50 | search: el.dataset.search, 51 | selected: Object.values(this.selected).includes(el.value) 52 | } 53 | this.options.push(newel); 54 | 55 | }) 56 | 57 | 58 | })); 59 | }, 60 | // clear search field 61 | clear() { 62 | this.search = '' 63 | }, 64 | // deselect selected options 65 | deselect() { 66 | setTimeout(() => { 67 | this.selected = [] 68 | this.selectedElms = [] 69 | Object.keys(this.options).forEach((key) => { 70 | this.options[key].selected = false; 71 | }) 72 | }, 100) 73 | }, 74 | // select given option 75 | select(index, event) { 76 | if (!this.options[index].selected) { 77 | this.options[index].selected = true; 78 | this.options[index].element = event.target; 79 | this.selected.push(this.options[index].value); 80 | this.selectedElms.push(this.options[index]); 81 | 82 | } else { 83 | this.selected.splice(this.selected.lastIndexOf(index), 1); 84 | this.options[index].selected = false 85 | Object.keys(this.selectedElms).forEach((key) => { 86 | if (this.selectedElms[key].value == this.options[index].value) { 87 | setTimeout(() => { 88 | this.selectedElms.splice(key, 1) 89 | }, 100) 90 | } 91 | }) 92 | } 93 | }, 94 | // remove from selected option 95 | remove(index, option) { 96 | this.selectedElms.splice(index, 1); 97 | Object.keys(this.selected).forEach((skey) => { 98 | if (this.selected[skey] == option.value) { 99 | this.selected.splice(skey, 1); 100 | } 101 | }); 102 | Object.keys(this.options).forEach((key) => { 103 | if (this.options[key].value == option.value) { 104 | this.options[key].selected = false; 105 | } 106 | }); 107 | }, 108 | // filter out selected elements 109 | selectedElements() { 110 | return this.options.filter(op => op.selected === true) 111 | }, 112 | // get selected values 113 | selectedValues() { 114 | return this.options.filter(op => op.selected === true).map(el => el.value) 115 | } 116 | })); 117 | }); -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Alpine.js MultiSelect 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 |

22 | Alpine.js MultiSelect 23 |

24 | This example uses 25 | 26 | Tailwind CSS 27 | , 28 | 29 | Heroicons 30 | 31 | and 32 | 33 | Alpine's Focus plugin 34 | . 35 |
36 | 37 |
38 | 39 | 40 | 62 | 63 |
64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 | 90 | 91 |
92 |
93 | 94 | 95 | 96 | more selected 97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 | 105 |
106 | 111 | 116 |
117 |
118 |
119 | 120 |
121 |
122 |
123 | 124 |
125 | 126 |
127 |
128 | 129 | 131 | 132 |
133 | 135 |
136 | 137 | Esc 138 | 139 | 140 | Del 141 | 142 |
143 |
144 |
145 | 146 |
    147 | 175 |
176 |
177 |
178 |
179 |
180 | 181 |
182 |
183 | 184 |
185 |
186 | 187 | 188 |
189 |
190 | 191 |
192 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## Welcome to GitHub Pages 2 | 3 | You can use the [editor on GitHub](https://github.com/alexpechkarev/alpinejs-multiselect/edit/main/docs/index.md) to maintain and preview the content for your website in Markdown files. 4 | 5 | Whenever you commit to this repository, GitHub Pages will run [Jekyll](https://jekyllrb.com/) to rebuild the pages in your site, from the content in your Markdown files. 6 | 7 | ### Markdown 8 | 9 | Markdown is a lightweight and easy-to-use syntax for styling your writing. It includes conventions for 10 | 11 | ```markdown 12 | Syntax highlighted code block 13 | 14 | # Header 1 15 | ## Header 2 16 | ### Header 3 17 | 18 | - Bulleted 19 | - List 20 | 21 | 1. Numbered 22 | 2. List 23 | 24 | **Bold** and _Italic_ and `Code` text 25 | 26 | [Link](url) and ![Image](src) 27 | ``` 28 | 29 | For more details see [Basic writing and formatting syntax](https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). 30 | 31 | ### Jekyll Themes 32 | 33 | Your Pages site will use the layout and styles from the Jekyll theme you have selected in your [repository settings](https://github.com/alexpechkarev/alpinejs-multiselect/settings/pages). The name of this theme is saved in the Jekyll `_config.yml` configuration file. 34 | 35 | ### Support or Contact 36 | 37 | Having trouble with Pages? Check out our [documentation](https://docs.github.com/categories/github-pages-basics/) or [contact support](https://support.github.com/contact) and we’ll help you sort it out. 38 | --------------------------------------------------------------------------------