├── LICENSE ├── README.md └── lwc ├── MultiSelect ├── MultiSelect.html ├── MultiSelect.js └── MultiSelect.js-meta.xml ├── MultiSelectItem ├── MultiSelectItem.css ├── MultiSelectItem.html ├── MultiSelectItem.js └── MultiSelectItem.js-meta.xml └── pillContainer ├── pillContainer.html ├── pillContainer.js └── pillContainer.js-meta.xml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Caspar Harmer 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 | # MultiSelect 2 | Same as my Aura MultiSelect, except uses pills to show selected items. 3 | 4 | # Update 2020-09-17 5 | Renamed to MultiSelect from lwcMultiSelect as lwc now default. Will update code to not use pills if so desired. 6 | 7 | # Update 2020-09-16 8 | Salesforce changed it's design system to make pills a block display, which means they don't work inside the multiselect, so I've added a custom pill component 9 | to this project - which means it can be used the same as before. This will be needed for all implementations of this component, as the change made by Salesforce completely breaks it. 10 | 11 | Just add the new pill component, and add to the multiselect in place of the pill container, and everything else is the same. 12 | 13 | To use in aura, add this markup: 14 | 15 | 23 | 24 | 25 | 26 | Handle in the controller like this: 27 | 28 | handleSelectionChange(event){ 29 | event.stopPropagation(); 30 | const detail = event.detail; 31 | //semi-colon seperated string 32 | const selectedValue = detail.value; 33 | } 34 | 35 | I think that's all you need. 36 | 37 | 38 | In action: 39 | 40 | [![Multiselect gif][1]][1] 41 | 42 | [1]: https://media.giphy.com/media/l0Qlzg9JJcujQc0KrE/giphy.gif 43 | 44 | -------------------------------------------------------------------------------- /lwc/MultiSelect/MultiSelect.html: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /lwc/MultiSelect/MultiSelect.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | 3 | export default class MultiSelect extends LightningElement { 4 | 5 | @api width = 100; 6 | @api variant = ''; 7 | @api label = ''; 8 | @api name = ''; 9 | @api dropdownLength = 5; 10 | 11 | @track options = [{label:'Docksta table',value:'Docksta table',selected:false}, 12 | {label:'Ektorp sofa',value:'Ektorp sofa',selected:false}, 13 | {label:'Poäng armchair',value:'Poäng armchair',selected:false}, 14 | {label:'Kallax shelving',value:'Kallax shelving',selected:false}, 15 | {label:'Billy bookcase',value:'Billy bookcase',selected:false}, 16 | {label:'Landskrona sofa',value:'Landskrona sofa',selected:false}, 17 | {label:'Krippan loveseat',value:'Krippan loveseat',selected:false}]; 18 | @track value_ = ''; //serialized value - ie 'CA;FL;IL' used when / if options have not been set yet 19 | @track isOpen = false; 20 | @api selectedPills = []; //seperate from values, because for some reason pills use {label,name} while values uses {label:value} 21 | 22 | rendered = false; 23 | 24 | @api 25 | get options(){ 26 | return this.options_ 27 | } 28 | set options(options){ 29 | this.rendered = false; 30 | this.parseOptions(options); 31 | this.parseValue(this.value_); 32 | } 33 | 34 | @api 35 | get value(){ 36 | let selectedValues = this.selectedValues(); 37 | return selectedValues.length > 0 ? selectedValues.join(";") : ""; 38 | } 39 | set value(value){ 40 | this.value_ = value; 41 | this.parseValue(value); 42 | 43 | } 44 | 45 | parseValue(value){ 46 | if (!value || !this.options_ || this.options_.length < 1){ 47 | return; 48 | } 49 | var values = value.split(";"); 50 | var valueSet = new Set(values); 51 | 52 | this.options_ = this.options_.map(function(option) { 53 | if (valueSet.has(option.value)){ 54 | option.selected = true; 55 | } 56 | return option; 57 | }); 58 | this.selectedPills = this.getPillArray(); 59 | } 60 | 61 | parseOptions(options){ 62 | if (options != undefined && Array.isArray(options)){ 63 | this.options_ = JSON.parse(JSON.stringify(options)).map( (option,i) => { 64 | option.key = i; 65 | return option; 66 | }); 67 | } 68 | } 69 | 70 | 71 | //private called by getter of 'value' 72 | selectedValues(){ 73 | var values = []; 74 | //if no options set yet or invalid, just return value 75 | if (this.options_.length < 1){ 76 | return this.value_; 77 | } 78 | this.options_.forEach(function(option) { 79 | if (option.selected === true) { 80 | values.push(option.value); 81 | } 82 | }); 83 | return values; 84 | } 85 | 86 | connectedCallback() { 87 | } 88 | 89 | renderedCallback(){ 90 | } 91 | 92 | 93 | 94 | get labelStyle() { 95 | return this.variant === 'label-hidden' ? ' slds-hide' : ' slds-form-element__label ' ; 96 | } 97 | 98 | get dropdownOuterStyle(){ 99 | return 'slds-dropdown slds-dropdown_fluid slds-dropdown_length-5' + this.dropdownLength; 100 | } 101 | 102 | get mainDivClass(){ 103 | var style = ' slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click '; 104 | return this.isOpen ? ' slds-is-open ' + style : style; 105 | } 106 | get hintText(){ 107 | if (this.selectedPills.length === 0) { 108 | return "Select an option..."; 109 | } 110 | return ""; 111 | } 112 | 113 | openDropdown(){ 114 | this.isOpen = true; 115 | } 116 | closeDropdown(){ 117 | this.isOpen = false; 118 | } 119 | 120 | /* following pair of functions are a clever way of handling a click outside, 121 | despite us not having access to the outside dom. 122 | see: https://salesforce.stackexchange.com/questions/255691/handle-click-outside-element-in-lwc 123 | I made a slight improvement - by calling stopImmediatePropagation, I avoid the setTimeout call 124 | that the original makes to break the event flow. 125 | */ 126 | handleClick(event){ 127 | event.stopImmediatePropagation(); 128 | this.openDropdown(); 129 | window.addEventListener('click', this.handleClose); 130 | } 131 | handleClose = (event) => { 132 | event.stopPropagation(); 133 | this.closeDropdown(); 134 | window.removeEventListener('click', this.handleClose); 135 | } 136 | 137 | handlePillRemove(event){ 138 | event.preventDefault(); 139 | event.stopPropagation(); 140 | 141 | const name = event.detail.item.name; 142 | 143 | this.options_.forEach(function(element) { 144 | if (element.value === name) { 145 | element.selected = false; 146 | } 147 | }); 148 | this.selectedPills = this.getPillArray(); 149 | this.despatchChangeEvent(); 150 | 151 | } 152 | 153 | handleSelectedClick(event){ 154 | 155 | var value; 156 | var selected; 157 | event.preventDefault(); 158 | event.stopPropagation(); 159 | 160 | const listData = event.detail; 161 | 162 | value = listData.value; 163 | selected = listData.selected; 164 | 165 | //shift key ADDS to the list (unless clicking on a previously selected item) 166 | //also, shift key does not close the dropdown. 167 | if (listData.shift) { 168 | this.options_.forEach(function(option) { 169 | if (option.value === value) { 170 | option.selected = selected === true ? false : true; 171 | } 172 | }); 173 | } 174 | else { 175 | this.options_.forEach(function(option) { 176 | if (option.value === value) { 177 | option.selected = selected === "true" ? false : true; 178 | } else { 179 | option.selected = false; 180 | } 181 | }); 182 | this.closeDropdown(); 183 | } 184 | 185 | this.selectedPills = this.getPillArray(); 186 | this.despatchChangeEvent(); 187 | 188 | } 189 | 190 | 191 | despatchChangeEvent() { 192 | let values = this.selectedValues(); 193 | let valueString = values.length > 0 ? values.join(";") : ""; 194 | const eventDetail = {value:valueString}; 195 | const changeEvent = new CustomEvent('change', { detail: eventDetail }); 196 | this.dispatchEvent(changeEvent); 197 | } 198 | 199 | 200 | getPillArray(){ 201 | var pills = []; 202 | this.options_.forEach(function(element) { 203 | var interator = 0; 204 | if (element.selected) { 205 | pills.push({label:element.label, name:element.value, key: interator++}); 206 | } 207 | }); 208 | return pills; 209 | } 210 | 211 | 212 | } 213 | -------------------------------------------------------------------------------- /lwc/MultiSelect/MultiSelect.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | false 5 | 6 | -------------------------------------------------------------------------------- /lwc/MultiSelectItem/MultiSelectItem.css: -------------------------------------------------------------------------------- 1 | .no-selection { 2 | 3 | -webkit-touch-callout: none; /* iOS Safari */ 4 | -webkit-user-select: none; /* Safari */ 5 | -khtml-user-select: none; /* Konqueror HTML */ 6 | -moz-user-select: none; /* Firefox */ 7 | -ms-user-select: none; /* Internet Explorer/Edge */ 8 | user-select: none; /* Non-prefixed version, currently 9 | supported by Chrome and Opera */ 10 | } 11 | -------------------------------------------------------------------------------- /lwc/MultiSelectItem/MultiSelectItem.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lwc/MultiSelectItem/MultiSelectItem.js: -------------------------------------------------------------------------------- 1 | import { LightningElement,api } from 'lwc'; 2 | 3 | export default class MultiSelectItem extends LightningElement { 4 | @api key = ''; 5 | @api value = ''; 6 | @api label = ''; 7 | @api selected = false; 8 | 9 | get listItemStyle() { 10 | var baseStyles = ' slds-media slds-listbox__option_plain slds-media_small slds-listbox__option '; 11 | return this.selected === true ? baseStyles + ' slds-is-selected ' : baseStyles ; 12 | } 13 | 14 | selectHandler(event) { 15 | // Prevents the anchor element from navigating to a URL. 16 | event.preventDefault(); 17 | event.stopPropagation(); 18 | const selectedEvent = new CustomEvent('selected', { detail: {label:this.label,value:this.value,selected:this.selected,shift:event.shiftKey} }); 19 | this.dispatchEvent(selectedEvent); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lwc/MultiSelectItem/MultiSelectItem.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | false 5 | 6 | -------------------------------------------------------------------------------- /lwc/pillContainer/pillContainer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lwc/pillContainer/pillContainer.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track, api } from 'lwc'; 2 | 3 | export default class PillContainer extends LightningElement { 4 | 5 | @track items_ = []; 6 | rendered = false; 7 | 8 | @api 9 | get items() { 10 | return this.items_; 11 | } 12 | set items(values) { 13 | 14 | //console.log(JSON.parse(JSON.stringify(values))); 15 | let tempitems = JSON.parse(JSON.stringify(values)); 16 | tempitems.forEach( (pill,index) => { 17 | pill.internalKey = index; 18 | }); 19 | 20 | this.items_ = tempitems; 21 | 22 | } 23 | 24 | deletePill = (event) => { 25 | let key = event.target.dataset.key; 26 | this.items_.some( (pill,index) => { 27 | if (pill.internalKey == key){ 28 | this.items_.splice(index, 1); 29 | this.despatchItemRemoveEventEvent(pill); 30 | return true; 31 | } 32 | return false; 33 | }); 34 | } 35 | 36 | despatchItemRemoveEventEvent(pill) { 37 | const eventDetail = { item: pill }; 38 | const changeEvent = new CustomEvent("itemremove", { detail: eventDetail }); 39 | this.dispatchEvent(changeEvent); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /lwc/pillContainer/pillContainer.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 49.0 4 | false 5 | --------------------------------------------------------------------------------