class SearchableMulti extends HTMLElement { static get observedAttributes() { return ['placeholder']; } constructor() { super(); this._values = []; this._placeholder = 'Search...'; } connectedCallback() { if(!this._rendered) { this._rendered = true; this.attachShadow({ mode: 'open' }); this.shadowRoot.appendChild(this._template()); this._refresh(); } this._nonSelected.addEventListener('click', this); this._selected.addEventListener('click', this); this._search.addEventListener('keyup', this); } disconnectedCallback() { this._nonSelected.removeEventListener('click', this); this._selected.removeEventListener('click', this); this._search.removeEventListener('keyup', this); } attributeChangedCallback(name, oldVal, newVal) { if(name === 'placeholder') { this.placeholder = newVal; } } get value() { return this._values; } get placeholder() { return this._placeholder; } set placeholder(val) { this._placeholder = val; if(this._rendered) { this.shadowRoot.querySelector('input').placeholder = val; } } handleEvent(ev) { var el = ev.target; switch(ev.type) { case 'click': if(el.className === 'item') { if(el.parentNode.className === 'non-selected-wrapper') { this._nonSelectedClick(el); } else { this._selectedClick(el); } } break; case 'keyup': if(ev.keyCode === 32 || ev.keyCode === 13) { if(el.className === 'item') { if(el.parentNode.className === 'non-selected-wrapper') { this._nonSelectedClick(el); } else { this._selectedClick(el); } ev.preventDefault(); } } else { this._onSearch(); } break; } } _nonSelectedClick(el) { // Not already selected if(!el._selected) { this._setSelected(el); this.dispatchEvent(new Event('change')); } } _setSelected(el) { el._option.selected = true; var clone = el._selected = el.cloneNode(true); clone._nonSelected = el; this._selected.appendChild(clone); this._values.push(el.dataset.value); } _selectedClick(el) { var nonSelected = el._nonSelected; var option = nonSelected._option; nonSelected._selected = undefined; el.parentNode.removeChild(el); // Deselect the option option.selected = false; // Remove from values var idx = this._values.indexOf(el.dataset.value); if(idx !== -1) { this._values.splice(idx, 1); this.dispatchEvent(new Event('change')); } } _onSearch() { var term = this._search.value.toLowerCase(); function includes(str) { return str.toLowerCase().indexOf(term) !== -1; } var nonSelected, d; for(var i = 0, len = this._nonSelected.children.length; i < len; i++) { nonSelected = this._nonSelected.children[i]; if(term && !includes(nonSelected.dataset.value) && !includes(nonSelected.textContent)) { d = 'none'; } else { d = ''; } nonSelected.style.display = d; if(nonSelected._selected) { nonSelected._selected.style.display = d; } } } _template() { var doc = this.ownerDocument; var wrapper = doc.createElement('div'); wrapper.className = 'wrapper'; var style = doc.createElement('style'); style.textContent = this._styles(); var input = this._search = doc.createElement('input'); input.type = 'text'; input.className = 'search-input'; input.placeholder = this.placeholder; var nonSelected = this._nonSelected = doc.createElement('div'); nonSelected.className = 'non-selected-wrapper'; var selected = this._selected = doc.createElement('div'); selected.className = 'selected-wrapper'; wrapper.appendChild(style); wrapper.appendChild(input); wrapper.appendChild(nonSelected); wrapper.appendChild(selected); return wrapper; } _styles() { return ` :host { display: block; } .wrapper { border: 1px solid #ccc; border-radius: 3px; overflow: hidden; width: 100%; } .non-selected-wrapper, .selected-wrapper { box-sizing: border-box; display: inline-block; height: 200px; overflow-y: scroll;; padding: 10px; vertical-align: top; width: 50%; } .non-selected-wrapper { background: #fafafa; border-right: 1px solid #ccc; } .selected-wrapper { background: #fff; } .item { cursor: pointer; display: block; padding: 5px 10px; } .item:hover { background: #ececec; border-radius: 2px; } .search-input { border: 0; border-bottom: 1px solid #ccc; border-radius: 0; display: block; font-size: 1em; margin: 0; outline: 0; padding: 10px 20px; width: 100%; } .non-selected-wrapper .item.selected { opacity: 0.5; } .non-selected-wrapper .row.selected:hover { background: inherit; cursor: inherit; } `; } _refresh() { this._selected.innerHTML = this._nonSelected.innerHTML = ''; var term = this._search.value; var options = [].slice.call(this.querySelectorAll('option')); var doc = this.ownerDocument; options.forEach(function(option){ var row = doc.createElement('a'); row.setAttribute('tabindex', "0"); row.setAttribute('role', 'button'); row.textContent = option.textContent; row.dataset.value = option.value; row.className = 'item'; row._option = option; this._nonSelected.appendChild(row); if(option.selected) { this._setSelected(row); } }.bind(this)); } } customElements.define('searchable-multi', SearchableMulti);