` 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 |
100 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | multi-input
6 |
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 |
--------------------------------------------------------------------------------