├── .glitch-assets ├── README.md ├── index.html ├── multi-input.js ├── script.js └── style.css /.glitch-assets: -------------------------------------------------------------------------------- 1 | {"name":"drag-in-files.svg","date":"2016-10-22T16:17:49.954Z","url":"https://cdn.hyperdev.com/drag-in-files.svg","type":"image/svg","size":7646,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/drag-in-files.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(102, 153, 205)","uuid":"adSBq97hhhpFNUna"} 2 | {"name":"click-me.svg","date":"2016-10-23T16:17:49.954Z","url":"https://cdn.hyperdev.com/click-me.svg","type":"image/svg","size":7116,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/click-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(243, 185, 186)","uuid":"adSBq97hhhpFNUnb"} 3 | {"name":"paste-me.svg","date":"2016-10-24T16:17:49.954Z","url":"https://cdn.hyperdev.com/paste-me.svg","type":"image/svg","size":7242,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/paste-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(42, 179, 185)","uuid":"adSBq97hhhpFNUnc"} 4 | {"uuid":"adSBq97hhhpFNUna","deleted":true} 5 | {"uuid":"adSBq97hhhpFNUnb","deleted":true} 6 | {"uuid":"adSBq97hhhpFNUnc","deleted":true} 7 | {"name":"favicon.ico","date":"2019-06-10T15:10:14.744Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Ffavicon.ico","type":"image/vnd.microsoft.icon","size":18799,"imageWidth":256,"imageHeight":256,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Ffavicon.ico","thumbnailWidth":256,"thumbnailHeight":256,"uuid":"hl59tAnmiilfJ198"} 8 | {"uuid":"hl59tAnmiilfJ198","deleted":true} 9 | {"name":"favicon.ico","date":"2019-06-10T15:14:18.753Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Ffavicon.ico","type":"image/vnd.microsoft.icon","size":18799,"imageWidth":256,"imageHeight":256,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Ffavicon.ico","thumbnailWidth":256,"thumbnailHeight":256,"uuid":"1aOwQu6pFrgHMIzl"} 10 | {"uuid":"1aOwQu6pFrgHMIzl","deleted":true} 11 | {"name":"multi-input.gif","date":"2019-06-11T12:58:05.473Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input.gif","type":"image/gif","size":29259,"imageWidth":421,"imageHeight":233,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input.gif","thumbnailWidth":330,"thumbnailHeight":183,"uuid":"NFdiCY1eH2IA2m6n"} 12 | {"uuid":"NFdiCY1eH2IA2m6n","deleted":true} 13 | {"name":"multi-input.gif","date":"2019-06-11T13:01:21.008Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input.gif","type":"image/gif","size":50744,"imageWidth":421,"imageHeight":233,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input.gif","thumbnailWidth":330,"thumbnailHeight":183,"uuid":"WzB8S7qGYOvUFiQ8"} 14 | {"name":"multi-input.mp4","date":"2019-06-11T14:53:43.071Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input.mp4","type":"video/mp4","size":251427,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input.mp4","thumbnailWidth":210,"thumbnailHeight":210,"uuid":"7goW9Z21jSCIINGU"} 15 | {"name":"multi-input.gif","date":"2019-06-11T15:04:45.035Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input.gif","type":"image/gif","size":82367,"imageWidth":1440,"imageHeight":900,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input.gif","thumbnailWidth":330,"thumbnailHeight":207,"uuid":"fEEwJe7yZGezIOUQ"} 16 | {"uuid":"fEEwJe7yZGezIOUQ","deleted":true} 17 | {"name":"multi-input.gif","date":"2019-06-11T15:09:34.980Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input.gif","type":"image/gif","size":523358,"imageWidth":1440,"imageHeight":900,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input.gif","thumbnailWidth":330,"thumbnailHeight":207,"uuid":"lxDMTQn82vJmxOGN"} 18 | {"uuid":"lxDMTQn82vJmxOGN","deleted":true} 19 | {"uuid":"WzB8S7qGYOvUFiQ8","deleted":true} 20 | {"name":"multi-input.gif","date":"2019-06-11T15:14:20.751Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input.gif","type":"image/gif","size":178686,"imageWidth":814,"imageHeight":502,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input.gif","thumbnailWidth":330,"thumbnailHeight":204,"uuid":"ZHUdEbW9FabvC1Yq"} 21 | {"name":"multi-input.png","date":"2019-06-12T15:10:03.239Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input.png","type":"image/png","size":18226,"imageWidth":680,"imageHeight":306,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input.png","thumbnailWidth":330,"thumbnailHeight":149,"uuid":"XEg0tZiXdwD98hva"} 22 | {"name":"multi-input-50%.png","date":"2019-06-12T15:13:37.085Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input-50%25.png","type":"image/png","size":4383,"imageWidth":340,"imageHeight":153,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input-50%25.png","thumbnailWidth":330,"thumbnailHeight":149,"uuid":"yhkfKPx0cZsb7zJq"} 23 | {"uuid":"yhkfKPx0cZsb7zJq","deleted":true} 24 | {"uuid":"XEg0tZiXdwD98hva","deleted":true} 25 | {"name":"multi-input-50%.png","date":"2019-06-12T15:16:44.334Z","url":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fmulti-input-50%25.png","type":"image/png","size":11933,"imageWidth":340,"imageHeight":153,"thumbnail":"https://cdn.glitch.com/dda744c5-58a9-4809-897c-68396377983a%2Fthumbnails%2Fmulti-input-50%25.png","thumbnailWidth":330,"thumbnailHeight":149,"uuid":"sgdh1cO3mpHb7WuB"} 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # <multi-input> 2 | 3 | multi-input custom element showing selection of multiple Shakespeare characters 4 | 5 | Custom element for selecting multiple items using an `input` and `datalist` to suggest options. 6 | 7 | Delete items with Backspace or by tapping/clicking an item's × icon. 8 | 9 |
10 | 11 | | [View and remix this project live on Glitch](https://glitch.com/~multi-input) | 12 | | --- | 13 | 14 |
15 | 16 | ## Usage 17 | 18 | 1. Add [multi-input.js](https://github.com/samdutton/multi-input/blob/glitch/multi-input.js) to your project and link to it: 19 | 20 | ```html 21 | 22 | ``` 23 | 24 | 2. Wrap an `input` and a `datalist` in a `` (see [index.html](https://github.com/samdutton/multi-input/blob/glitch/index.html#L14)): 25 | 26 | ```html 27 | 28 | 29 | 30 | 31 | 32 | ... 33 | 34 | 35 | ``` 36 | 37 | 3. Get selected values like this (see [script.js](https://github.com/samdutton/multi-input/blob/glitch/script.js)): 38 | 39 | ```js 40 | const getButton = document.getElementById('get'); 41 | const multiInput = document.querySelector('multi-input'); 42 | getButton.onclick = () => { 43 | console.log(multiInput.getValues()); 44 | } 45 | ``` 46 |
47 | 48 | ## Can I style the components? 49 | 50 | Sure. 51 | 52 | There are several custom properties: 53 | 54 | ``` 55 | --multi-input-border 56 | --multi-input-item-bg-color 57 | --multi-input-item-border 58 | --multi-input-item-font-size 59 | --multi-input-input-font-size 60 | ``` 61 | 62 | Style components like this: 63 | 64 | ``` css 65 | multi-input { 66 | --multi-input-border: 2px solid red; 67 | } 68 | ``` 69 | 70 | ## Known issues 71 | 72 | ### Problems with shadow DOM CSS pseudo classes 73 | 74 | Two selectors have been added outside the shadow DOM using a `multi-input` selector: 75 | 76 | * `::slotted(input)::-webkit-calendar-picker-indicator` doesn't work in any browser. 77 | * `::slotted(div.item)::after` doesn't work in Safari. 78 | 79 |
80 | 81 | ## My platform doesn't support custom elements :^| 82 | 83 | Custom elements are [widely supported by modern browsers](https://caniuse.com/#search=custom%20elements). 84 | 85 | However, `` simply wraps an `input` element that has a `datalist`, so behaviour will degrade gracefully to a 'normal' `datalist` experience in browsers without custom element support. 86 | 87 |
88 | 89 | ## My platform doesn't support datalist :^| :^| 90 | 91 | The `datalist` element is [supported by all modern browsers](https://caniuse.com/#feat=datalist). 92 | 93 | If your target browser doesn't support `datalist`, behaviour will fall back to the plain old `input` experience. 94 | 95 |
96 | 97 | ## Obligatory screencast 98 | 99 | Screencast showing Shakespeare character names being selected via a multi-input custom element 100 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | multi-input 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /multi-input.js: -------------------------------------------------------------------------------- 1 | class MultiInput extends HTMLElement { 2 | constructor() { 3 | super(); 4 | // This is a hack :^(. 5 | // ::slotted(input)::-webkit-calendar-picker-indicator doesn't work in any browser. 6 | // ::slotted() with ::after doesn't work in Safari. 7 | this.innerHTML += 8 | ``; 25 | this._shadowRoot = this.attachShadow({mode: 'open'}); 26 | this._shadowRoot.innerHTML = 27 | ` 60 | `; 61 | 62 | this._datalist = this.querySelector('datalist'); 63 | this._allowedValues = []; 64 | for (const option of this._datalist.options) { 65 | this._allowedValues.push(option.value); 66 | } 67 | 68 | this._input = this.querySelector('input'); 69 | this._input.onblur = this._handleBlur.bind(this); 70 | this._input.oninput = this._handleInput.bind(this); 71 | this._input.onkeydown = (event) => { 72 | this._handleKeydown(event); 73 | }; 74 | 75 | this._allowDuplicates = this.hasAttribute('allow-duplicates'); 76 | } 77 | 78 | // Called by _handleKeydown() when the value of the input is an allowed value. 79 | _addItem(value) { 80 | this._input.value = ''; 81 | const item = document.createElement('div'); 82 | item.classList.add('item'); 83 | item.textContent = value; 84 | this.insertBefore(item, this._input); 85 | item.onclick = () => { 86 | this._deleteItem(item); 87 | }; 88 | 89 | // Remove value from datalist options and from _allowedValues array. 90 | // Value is added back if an item is deleted (see _deleteItem()). 91 | if (!this._allowDuplicates) { 92 | for (const option of this._datalist.options) { 93 | if (option.value === value) { 94 | option.remove(); 95 | }; 96 | } 97 | this._allowedValues = 98 | this._allowedValues.filter((item) => item !== value); 99 | } 100 | } 101 | 102 | // Called when the × icon is tapped/clicked or 103 | // by _handleKeydown() when Backspace is entered. 104 | _deleteItem(item) { 105 | const value = item.textContent; 106 | item.remove(); 107 | // If duplicates aren't allowed, value is removed (in _addItem()) 108 | // as a datalist option and from the _allowedValues array. 109 | // So — need to add it back here. 110 | if (!this._allowDuplicates) { 111 | const option = document.createElement('option'); 112 | option.value = value; 113 | // Insert as first option seems reasonable... 114 | this._datalist.insertBefore(option, this._datalist.firstChild); 115 | this._allowedValues.push(value); 116 | } 117 | } 118 | 119 | // Avoid stray text remaining in the input element that's not in a div.item. 120 | _handleBlur() { 121 | this._input.value = ''; 122 | } 123 | 124 | // Called when input text changes, 125 | // either by entering text or selecting a datalist option. 126 | _handleInput() { 127 | // Add a div.item, but only if the current value 128 | // of the input is an allowed value 129 | const value = this._input.value; 130 | if (this._allowedValues.includes(value)) { 131 | this._addItem(value); 132 | } 133 | } 134 | 135 | // Called when text is entered or keys pressed in the input element. 136 | _handleKeydown(event) { 137 | const itemToDelete = event.target.previousElementSibling; 138 | const value = this._input.value; 139 | // On Backspace, delete the div.item to the left of the input 140 | if (value ==='' && event.key === 'Backspace' && itemToDelete) { 141 | this._deleteItem(itemToDelete); 142 | // Add a div.item, but only if the current value 143 | // of the input is an allowed value 144 | } else if (this._allowedValues.includes(value)) { 145 | this._addItem(value); 146 | } 147 | } 148 | 149 | // Public method for getting item values as an array. 150 | getValues() { 151 | const values = []; 152 | const items = this.querySelectorAll('.item'); 153 | for (const item of items) { 154 | values.push(item.textContent); 155 | } 156 | return values; 157 | } 158 | } 159 | 160 | window.customElements.define('multi-input', MultiInput); 161 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const getButton = document.getElementById('get'); 2 | const multiInput = document.querySelector('multi-input'); 3 | const values = document.querySelector('#values'); 4 | 5 | getButton.onclick = () => { 6 | if (multiInput.getValues().length > 0) { 7 | values.textContent = `Got ${multiInput.getValues().join(' and ')}!`; 8 | } else { 9 | values.textContent = 'Got noone :`^(.'; 10 | } 11 | } 12 | 13 | document.querySelector('input').focus(); 14 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Google Sans', sans-serif; 3 | margin: 40px 4 | } 5 | 6 | multi-input { 7 | display: inline-block; 8 | margin: 0 20px 20px 0; 9 | } 10 | 11 | button { 12 | background-color: #eee; 13 | border: 1px solid #ddd; 14 | font-size: 16px; 15 | height: 30px; 16 | margin: 0 10px 20px 0; 17 | } 18 | 19 | body > div { 20 | align-items: center; 21 | display: flex; 22 | justify-content: center; 23 | } 24 | 25 | label { 26 | display: block; 27 | margin: 0 20px 20px 0; 28 | } 29 | 30 | p { 31 | text-align: center; 32 | } 33 | --------------------------------------------------------------------------------