@vaadin/multi-select-combo-box 23.1.0-alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +62 -0
- package/package.json +50 -0
- package/src/vaadin-multi-select-combo-box-chip.js +66 -0
- package/src/vaadin-multi-select-combo-box-container.js +52 -0
- package/src/vaadin-multi-select-combo-box-dropdown.js +38 -0
- package/src/vaadin-multi-select-combo-box-internal.js +161 -0
- package/src/vaadin-multi-select-combo-box-item.js +37 -0
- package/src/vaadin-multi-select-combo-box-overlay.js +34 -0
- package/src/vaadin-multi-select-combo-box-scroller.js +31 -0
- package/src/vaadin-multi-select-combo-box.d.ts +254 -0
- package/src/vaadin-multi-select-combo-box.js +599 -0
- package/theme/lumo/vaadin-multi-select-combo-box-chip-styles.js +64 -0
- package/theme/lumo/vaadin-multi-select-combo-box-styles.js +33 -0
- package/theme/lumo/vaadin-multi-select-combo-box.js +11 -0
- package/theme/material/vaadin-multi-select-combo-box-chip-styles.js +69 -0
- package/theme/material/vaadin-multi-select-combo-box-styles.js +37 -0
- package/theme/material/vaadin-multi-select-combo-box.js +11 -0
- package/vaadin-multi-select-combo-box.d.ts +1 -0
- package/vaadin-multi-select-combo-box.js +2 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 - 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import './vaadin-multi-select-combo-box-chip.js';
|
|
7
|
+
import './vaadin-multi-select-combo-box-container.js';
|
|
8
|
+
import './vaadin-multi-select-combo-box-internal.js';
|
|
9
|
+
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
10
|
+
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
11
|
+
import { processTemplates } from '@vaadin/component-base/src/templates.js';
|
|
12
|
+
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
|
|
13
|
+
import { InputController } from '@vaadin/field-base/src/input-controller.js';
|
|
14
|
+
import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
|
|
15
|
+
import { inputFieldShared } from '@vaadin/field-base/src/styles/input-field-shared-styles.js';
|
|
16
|
+
import { css, registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
17
|
+
|
|
18
|
+
const multiSelectComboBox = css`
|
|
19
|
+
[hidden] {
|
|
20
|
+
display: none !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host([has-value]) ::slotted(input:placeholder-shown) {
|
|
24
|
+
color: transparent !important;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:host([has-value]) [class$='container'] {
|
|
28
|
+
width: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
::slotted(input) {
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
flex: 1 0 4em;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[part~='chip'] {
|
|
37
|
+
flex: 0 1 auto;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:host([readonly]) [part~='chip'] {
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectComboBox], {
|
|
46
|
+
moduleId: 'vaadin-multi-select-combo-box-styles'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `<vaadin-multi-select-combo-box>` is a web component that wraps `<vaadin-combo-box>` and extends
|
|
51
|
+
* its functionality to allow selecting multiple items, in addition to basic features.
|
|
52
|
+
*
|
|
53
|
+
* ```html
|
|
54
|
+
* <vaadin-multi-select-combo-box id="comboBox"></vaadin-multi-select-combo-box>
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* ```js
|
|
58
|
+
* const comboBox = document.querySelector('#comboBox');
|
|
59
|
+
* comboBox.items = ['apple', 'banana', 'lemon', 'orange'];
|
|
60
|
+
* comboBox.selectedItems = ['lemon', 'orange'];
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* ### Styling
|
|
64
|
+
*
|
|
65
|
+
* The following shadow DOM parts are available for styling:
|
|
66
|
+
*
|
|
67
|
+
* Part name | Description
|
|
68
|
+
* -----------------------|----------------
|
|
69
|
+
* `chip` | Chip shown for every selected item
|
|
70
|
+
* `label` | The label element
|
|
71
|
+
* `input-field` | The element that wraps prefix, value and suffix
|
|
72
|
+
* `clear-button` | The clear button
|
|
73
|
+
* `error-message` | The error message element
|
|
74
|
+
* `helper-text` | The helper text element wrapper
|
|
75
|
+
* `required-indicator` | The `required` state indicator element
|
|
76
|
+
* `toggle-button` | The toggle button
|
|
77
|
+
*
|
|
78
|
+
* The following state attributes are available for styling:
|
|
79
|
+
*
|
|
80
|
+
* Attribute | Description
|
|
81
|
+
* -----------------------|-----------------
|
|
82
|
+
* `disabled` | Set to a disabled element
|
|
83
|
+
* `has-value` | Set when the element has a value
|
|
84
|
+
* `has-label` | Set when the element has a label
|
|
85
|
+
* `has-helper` | Set when the element has helper text or slot
|
|
86
|
+
* `has-error-message` | Set when the element has an error message
|
|
87
|
+
* `invalid` | Set when the element is invalid
|
|
88
|
+
* `focused` | Set when the element is focused
|
|
89
|
+
* `focus-ring` | Set when the element is keyboard focused
|
|
90
|
+
* `opened` | Set when the dropdown is open
|
|
91
|
+
* `readonly` | Set to a readonly element
|
|
92
|
+
*
|
|
93
|
+
* ### Internal components
|
|
94
|
+
*
|
|
95
|
+
* In addition to `<vaadin-multi-select-combo-box>` itself, the following internal
|
|
96
|
+
* components are themable:
|
|
97
|
+
*
|
|
98
|
+
* - `<vaadin-multi-select-combo-box-overlay>` - has the same API as `<vaadin-overlay>`.
|
|
99
|
+
* - `<vaadin-multi-select-combo-box-item>` - has the same API as `<vaadin-item>`.
|
|
100
|
+
* - `<vaadin-multi-select-combo-box-container>` - has the same API as `<vaadin-input-container>`.
|
|
101
|
+
*
|
|
102
|
+
* Note: the `theme` attribute value set on `<vaadin-multi-select-combo-box>` is
|
|
103
|
+
* propagated to these components.
|
|
104
|
+
*
|
|
105
|
+
* See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
|
|
106
|
+
*
|
|
107
|
+
* @fires {Event} change - Fired when the user commits a value change.
|
|
108
|
+
* @fires {CustomEvent} custom-values-set - Fired when the user sets a custom value.
|
|
109
|
+
* @fires {CustomEvent} filter-changed - Fired when the `filter` property changes.
|
|
110
|
+
* @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
|
|
111
|
+
* @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
|
|
112
|
+
*
|
|
113
|
+
* @extends HTMLElement
|
|
114
|
+
* @mixes ElementMixin
|
|
115
|
+
* @mixes ThemableMixin
|
|
116
|
+
* @mixes InputControlMixin
|
|
117
|
+
*/
|
|
118
|
+
class MultiSelectComboBox extends InputControlMixin(ThemableMixin(ElementMixin(PolymerElement))) {
|
|
119
|
+
static get is() {
|
|
120
|
+
return 'vaadin-multi-select-combo-box';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static get template() {
|
|
124
|
+
return html`
|
|
125
|
+
<div class="vaadin-multi-select-combo-box-container">
|
|
126
|
+
<div part="label">
|
|
127
|
+
<slot name="label"></slot>
|
|
128
|
+
<span part="required-indicator" aria-hidden="true" on-click="focus"></span>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<vaadin-multi-select-combo-box-internal
|
|
132
|
+
id="comboBox"
|
|
133
|
+
items="[[items]]"
|
|
134
|
+
item-id-path="[[itemIdPath]]"
|
|
135
|
+
item-label-path="[[itemLabelPath]]"
|
|
136
|
+
item-value-path="[[itemValuePath]]"
|
|
137
|
+
disabled="[[disabled]]"
|
|
138
|
+
readonly="[[readonly]]"
|
|
139
|
+
auto-open-disabled="[[autoOpenDisabled]]"
|
|
140
|
+
allow-custom-value="[[allowCustomValues]]"
|
|
141
|
+
data-provider="[[dataProvider]]"
|
|
142
|
+
filter="{{filter}}"
|
|
143
|
+
filtered-items="[[filteredItems]]"
|
|
144
|
+
opened="{{opened}}"
|
|
145
|
+
renderer="[[renderer]]"
|
|
146
|
+
theme$="[[theme]]"
|
|
147
|
+
on-combo-box-item-selected="_onComboBoxItemSelected"
|
|
148
|
+
on-change="_onComboBoxChange"
|
|
149
|
+
on-custom-value-set="_onCustomValueSet"
|
|
150
|
+
>
|
|
151
|
+
<vaadin-multi-select-combo-box-container
|
|
152
|
+
part="input-field"
|
|
153
|
+
readonly="[[readonly]]"
|
|
154
|
+
disabled="[[disabled]]"
|
|
155
|
+
invalid="[[invalid]]"
|
|
156
|
+
theme$="[[theme]]"
|
|
157
|
+
>
|
|
158
|
+
<slot name="input"></slot>
|
|
159
|
+
<div id="clearButton" part="clear-button" slot="suffix"></div>
|
|
160
|
+
<div id="toggleButton" class="toggle-button" part="toggle-button" slot="suffix"></div>
|
|
161
|
+
</vaadin-multi-select-combo-box-container>
|
|
162
|
+
</vaadin-multi-select-combo-box-internal>
|
|
163
|
+
|
|
164
|
+
<div part="helper-text">
|
|
165
|
+
<slot name="helper"></slot>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div part="error-message">
|
|
169
|
+
<slot name="error-message"></slot>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static get properties() {
|
|
176
|
+
return {
|
|
177
|
+
/**
|
|
178
|
+
* Set true to prevent the overlay from opening automatically.
|
|
179
|
+
* @attr {boolean} auto-open-disabled
|
|
180
|
+
*/
|
|
181
|
+
autoOpenDisabled: Boolean,
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A full set of items to filter the visible options from.
|
|
185
|
+
* The items can be of either `String` or `Object` type.
|
|
186
|
+
*/
|
|
187
|
+
items: {
|
|
188
|
+
type: Array
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The item property used for a visual representation of the item.
|
|
193
|
+
* @attr {string} item-label-path
|
|
194
|
+
*/
|
|
195
|
+
itemLabelPath: {
|
|
196
|
+
type: String
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Path for the value of the item. If `items` is an array of objects,
|
|
201
|
+
* this property is used as a string value for the selected item.
|
|
202
|
+
* @attr {string} item-value-path
|
|
203
|
+
*/
|
|
204
|
+
itemValuePath: {
|
|
205
|
+
type: String
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Path for the id of the item, used to detect whether the item is selected.
|
|
210
|
+
* @attr {string} item-id-path
|
|
211
|
+
*/
|
|
212
|
+
itemIdPath: {
|
|
213
|
+
type: String
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The list of selected items.
|
|
218
|
+
* Note: modifying the selected items creates a new array each time.
|
|
219
|
+
*/
|
|
220
|
+
selectedItems: {
|
|
221
|
+
type: Array,
|
|
222
|
+
value: () => [],
|
|
223
|
+
notify: true
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* True if the dropdown is open, false otherwise.
|
|
228
|
+
*/
|
|
229
|
+
opened: {
|
|
230
|
+
type: Boolean,
|
|
231
|
+
notify: true,
|
|
232
|
+
value: false,
|
|
233
|
+
reflectToAttribute: true
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Number of items fetched at a time from the data provider.
|
|
238
|
+
* @attr {number} page-size
|
|
239
|
+
*/
|
|
240
|
+
pageSize: {
|
|
241
|
+
type: Number,
|
|
242
|
+
value: 50,
|
|
243
|
+
observer: '_pageSizeChanged'
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Function that provides items lazily. Receives two arguments:
|
|
248
|
+
*
|
|
249
|
+
* - `params` - Object with the following properties:
|
|
250
|
+
* - `params.page` Requested page index
|
|
251
|
+
* - `params.pageSize` Current page size
|
|
252
|
+
* - `params.filter` Currently applied filter
|
|
253
|
+
*
|
|
254
|
+
* - `callback(items, size)` - Callback function with arguments:
|
|
255
|
+
* - `items` Current page of items
|
|
256
|
+
* - `size` Total number of items.
|
|
257
|
+
*/
|
|
258
|
+
dataProvider: {
|
|
259
|
+
type: Object,
|
|
260
|
+
observer: '_dataProviderChanged'
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* When true, the user can input a value that is not present in the items list.
|
|
265
|
+
* @attr {boolean} allow-custom-values
|
|
266
|
+
*/
|
|
267
|
+
allowCustomValues: {
|
|
268
|
+
type: Boolean,
|
|
269
|
+
value: false
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Custom function for rendering the content of every item.
|
|
274
|
+
* Receives three arguments:
|
|
275
|
+
*
|
|
276
|
+
* - `root` The `<vaadin-multi-select-combo-box-item>` internal container DOM element.
|
|
277
|
+
* - `comboBox` The reference to the `<vaadin-combo-box>` element.
|
|
278
|
+
* - `model` The object with the properties related with the rendered
|
|
279
|
+
* item, contains:
|
|
280
|
+
* - `model.index` The index of the rendered item.
|
|
281
|
+
* - `model.item` The item.
|
|
282
|
+
*/
|
|
283
|
+
renderer: Function,
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Filtering string the user has typed into the input field.
|
|
287
|
+
*/
|
|
288
|
+
filter: {
|
|
289
|
+
type: String,
|
|
290
|
+
value: '',
|
|
291
|
+
notify: true
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A subset of items, filtered based on the user input. Filtered items
|
|
296
|
+
* can be assigned directly to omit the internal filtering functionality.
|
|
297
|
+
* The items can be of either `String` or `Object` type.
|
|
298
|
+
*/
|
|
299
|
+
filteredItems: Array,
|
|
300
|
+
|
|
301
|
+
/** @protected */
|
|
302
|
+
_hasValue: {
|
|
303
|
+
type: Boolean,
|
|
304
|
+
value: false
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static get observers() {
|
|
310
|
+
return ['_selectedItemsChanged(selectedItems, selectedItems.*)'];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Used by `ClearButtonMixin` as a reference to the clear button element.
|
|
315
|
+
* @protected
|
|
316
|
+
* @return {!HTMLElement}
|
|
317
|
+
*/
|
|
318
|
+
get clearElement() {
|
|
319
|
+
return this.$.clearButton;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @protected */
|
|
323
|
+
get _chips() {
|
|
324
|
+
return this.shadowRoot.querySelectorAll('[part~="chip"]');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** @protected */
|
|
328
|
+
ready() {
|
|
329
|
+
super.ready();
|
|
330
|
+
|
|
331
|
+
this.addController(
|
|
332
|
+
new InputController(this, (input) => {
|
|
333
|
+
this._setInputElement(input);
|
|
334
|
+
this._setFocusElement(input);
|
|
335
|
+
this.stateTarget = input;
|
|
336
|
+
this.ariaTarget = input;
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
this.addController(new LabelledInputController(this.inputElement, this._labelController));
|
|
340
|
+
|
|
341
|
+
this._inputField = this.shadowRoot.querySelector('[part="input-field"]');
|
|
342
|
+
this.__updateChips();
|
|
343
|
+
|
|
344
|
+
processTemplates(this);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Returns true if the current input value satisfies all constraints (if any).
|
|
349
|
+
* @return {boolean}
|
|
350
|
+
*/
|
|
351
|
+
checkValidity() {
|
|
352
|
+
return this.required ? this._hasValue : true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Override method inherited from `DisabledMixin` to forward disabled to chips.
|
|
357
|
+
* @protected
|
|
358
|
+
* @override
|
|
359
|
+
*/
|
|
360
|
+
_disabledChanged(disabled, oldDisabled) {
|
|
361
|
+
super._disabledChanged(disabled, oldDisabled);
|
|
362
|
+
|
|
363
|
+
if (disabled || oldDisabled) {
|
|
364
|
+
this._chips.forEach((chip) => {
|
|
365
|
+
chip.toggleAttribute('disabled', disabled);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Override method inherited from `InputMixin` to forward the input to combo-box.
|
|
372
|
+
* @protected
|
|
373
|
+
* @override
|
|
374
|
+
*/
|
|
375
|
+
_inputElementChanged(input) {
|
|
376
|
+
super._inputElementChanged(input);
|
|
377
|
+
|
|
378
|
+
if (input) {
|
|
379
|
+
this.$.comboBox._setInputElement(input);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Override method inherited from `FocusMixin` to validate on blur.
|
|
385
|
+
* @param {boolean} focused
|
|
386
|
+
* @protected
|
|
387
|
+
*/
|
|
388
|
+
_setFocused(focused) {
|
|
389
|
+
super._setFocused(focused);
|
|
390
|
+
|
|
391
|
+
if (!focused) {
|
|
392
|
+
this.validate();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Override method inherited from `InputMixin`
|
|
398
|
+
* to keep attribute after clearing the input.
|
|
399
|
+
* @protected
|
|
400
|
+
* @override
|
|
401
|
+
*/
|
|
402
|
+
_toggleHasValue() {
|
|
403
|
+
super._toggleHasValue(this._hasValue);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** @private */
|
|
407
|
+
_pageSizeChanged(pageSize, oldPageSize) {
|
|
408
|
+
if (Math.floor(pageSize) !== pageSize || pageSize <= 0) {
|
|
409
|
+
this.pageSize = oldPageSize;
|
|
410
|
+
console.error('"pageSize" value must be an integer > 0');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.$.comboBox.pageSize = this.pageSize;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** @private */
|
|
417
|
+
_selectedItemsChanged(selectedItems) {
|
|
418
|
+
this._hasValue = Boolean(selectedItems && selectedItems.length);
|
|
419
|
+
|
|
420
|
+
this._toggleHasValue();
|
|
421
|
+
|
|
422
|
+
// Re-render chips
|
|
423
|
+
this.__updateChips();
|
|
424
|
+
|
|
425
|
+
// Re-render scroller
|
|
426
|
+
this.$.comboBox.$.dropdown._scroller.requestContentUpdate();
|
|
427
|
+
|
|
428
|
+
// Wait for chips to render
|
|
429
|
+
requestAnimationFrame(() => {
|
|
430
|
+
this.$.comboBox.$.dropdown._setOverlayWidth();
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** @private */
|
|
435
|
+
_getItemLabel(item, itemLabelPath) {
|
|
436
|
+
return item && Object.prototype.hasOwnProperty.call(item, itemLabelPath) ? item[itemLabelPath] : item;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** @private */
|
|
440
|
+
_findIndex(item, selectedItems, itemIdPath) {
|
|
441
|
+
if (itemIdPath && item) {
|
|
442
|
+
for (let index = 0; index < selectedItems.length; index++) {
|
|
443
|
+
if (selectedItems[index] && selectedItems[index][itemIdPath] === item[itemIdPath]) {
|
|
444
|
+
return index;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return -1;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return selectedItems.indexOf(item);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** @private */
|
|
454
|
+
__clearFilter() {
|
|
455
|
+
this.$.comboBox.clear();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @private */
|
|
459
|
+
__removeItem(item) {
|
|
460
|
+
const itemsCopy = [...this.selectedItems];
|
|
461
|
+
itemsCopy.splice(itemsCopy.indexOf(item), 1);
|
|
462
|
+
this.__updateSelection(itemsCopy);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** @private */
|
|
466
|
+
__selectItem(item) {
|
|
467
|
+
const itemsCopy = [...this.selectedItems];
|
|
468
|
+
|
|
469
|
+
const index = this._findIndex(item, itemsCopy, this.itemIdPath);
|
|
470
|
+
if (index !== -1) {
|
|
471
|
+
// Do not unselect when manually typing and committing an already selected item.
|
|
472
|
+
if (this.filter.toLowerCase() === this._getItemLabel(item, this.itemLabelPath).toLowerCase()) {
|
|
473
|
+
this.__clearFilter();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
itemsCopy.splice(index, 1);
|
|
478
|
+
} else {
|
|
479
|
+
itemsCopy.push(item);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
this.__updateSelection(itemsCopy);
|
|
483
|
+
|
|
484
|
+
// Suppress `value-changed` event.
|
|
485
|
+
this.__clearFilter();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** @private */
|
|
489
|
+
__updateSelection(selectedItems) {
|
|
490
|
+
this.selectedItems = selectedItems;
|
|
491
|
+
|
|
492
|
+
this.validate();
|
|
493
|
+
|
|
494
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** @private */
|
|
498
|
+
__createChip(item) {
|
|
499
|
+
const chip = document.createElement('vaadin-multi-select-combo-box-chip');
|
|
500
|
+
chip.setAttribute('part', 'chip');
|
|
501
|
+
chip.setAttribute('slot', 'prefix');
|
|
502
|
+
|
|
503
|
+
chip.item = item;
|
|
504
|
+
chip.label = this._getItemLabel(item, this.itemLabelPath);
|
|
505
|
+
chip.toggleAttribute('disabled', this.disabled);
|
|
506
|
+
|
|
507
|
+
chip.addEventListener('item-removed', (e) => this._onItemRemoved(e));
|
|
508
|
+
chip.addEventListener('mousedown', (e) => this._preventBlur(e));
|
|
509
|
+
|
|
510
|
+
return chip;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** @private */
|
|
514
|
+
__updateChips() {
|
|
515
|
+
if (!this._inputField) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this._chips.forEach((chip) => {
|
|
520
|
+
chip.remove();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const items = [...this.selectedItems];
|
|
524
|
+
|
|
525
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
526
|
+
const chip = this.__createChip(items[i]);
|
|
527
|
+
this._inputField.insertBefore(chip, this._inputField.firstElementChild);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Override method inherited from `ClearButtonMixin` and clear items.
|
|
533
|
+
* @protected
|
|
534
|
+
* @override
|
|
535
|
+
*/
|
|
536
|
+
_onClearButtonClick(event) {
|
|
537
|
+
event.stopPropagation();
|
|
538
|
+
|
|
539
|
+
this.__updateSelection([]);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Override an event listener from `KeyboardMixin`.
|
|
544
|
+
* @param {KeyboardEvent} event
|
|
545
|
+
* @protected
|
|
546
|
+
* @override
|
|
547
|
+
*/
|
|
548
|
+
_onKeyDown(event) {
|
|
549
|
+
const items = this.selectedItems || [];
|
|
550
|
+
if (!this.readonly && event.key === 'Backspace' && items.length && this.inputElement.value === '') {
|
|
551
|
+
this.__removeItem(items[items.length - 1]);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** @private */
|
|
556
|
+
_onComboBoxChange() {
|
|
557
|
+
const item = this.$.comboBox.selectedItem;
|
|
558
|
+
if (item) {
|
|
559
|
+
this.__selectItem(item);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** @private */
|
|
564
|
+
_onComboBoxItemSelected(event) {
|
|
565
|
+
this.__selectItem(event.detail.item);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** @private */
|
|
569
|
+
_onCustomValueSet(event) {
|
|
570
|
+
// Do not set combo-box value
|
|
571
|
+
event.preventDefault();
|
|
572
|
+
|
|
573
|
+
this.__clearFilter();
|
|
574
|
+
|
|
575
|
+
this.dispatchEvent(
|
|
576
|
+
new CustomEvent('custom-values-set', {
|
|
577
|
+
detail: event.detail,
|
|
578
|
+
composed: true,
|
|
579
|
+
bubbles: true
|
|
580
|
+
})
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** @private */
|
|
585
|
+
_onItemRemoved(event) {
|
|
586
|
+
this.__removeItem(event.detail.item);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** @private */
|
|
590
|
+
_preventBlur(event) {
|
|
591
|
+
// Prevent mousedown event to keep the input focused
|
|
592
|
+
// and keep the overlay opened when clicking a chip.
|
|
593
|
+
event.preventDefault();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
customElements.define(MultiSelectComboBox.is, MultiSelectComboBox);
|
|
598
|
+
|
|
599
|
+
export { MultiSelectComboBox };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 - 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import '@vaadin/vaadin-lumo-styles/color.js';
|
|
7
|
+
import '@vaadin/vaadin-lumo-styles/font-icons.js';
|
|
8
|
+
import '@vaadin/vaadin-lumo-styles/spacing.js';
|
|
9
|
+
import '@vaadin/vaadin-lumo-styles/style.js';
|
|
10
|
+
import '@vaadin/vaadin-lumo-styles/typography.js';
|
|
11
|
+
import { fieldButton } from '@vaadin/vaadin-lumo-styles/mixins/field-button.js';
|
|
12
|
+
import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
13
|
+
|
|
14
|
+
const chip = css`
|
|
15
|
+
:host {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
align-self: center;
|
|
19
|
+
font-family: var(--lumo-font-family);
|
|
20
|
+
font-size: var(--lumo-font-size-xxs);
|
|
21
|
+
line-height: 1;
|
|
22
|
+
padding: 0.3125em 0 0.3125em calc(0.5em + var(--lumo-border-radius-s) / 4);
|
|
23
|
+
border-radius: var(--lumo-border-radius-s);
|
|
24
|
+
border-radius: var(--lumo-border-radius);
|
|
25
|
+
background-color: var(--lumo-contrast-20pct);
|
|
26
|
+
cursor: var(--lumo-clickable-cursor);
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[part='label'] {
|
|
33
|
+
color: var(--lumo-body-text-color);
|
|
34
|
+
font-weight: 500;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
text-overflow: ellipsis;
|
|
37
|
+
line-height: 1.25;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[part='remove-button'] {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
margin-top: -0.3125em;
|
|
45
|
+
margin-bottom: -0.3125em;
|
|
46
|
+
width: var(--lumo-icon-size-s);
|
|
47
|
+
height: var(--lumo-icon-size-s);
|
|
48
|
+
font-size: 1.5em;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[part='remove-button']::before {
|
|
52
|
+
content: var(--lumo-icons-cross);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:host([disabled]) [part] {
|
|
56
|
+
color: var(--lumo-disabled-text-color);
|
|
57
|
+
-webkit-text-fill-color: var(--lumo-disabled-text-color);
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
registerStyles('vaadin-multi-select-combo-box-chip', [fieldButton, chip], {
|
|
63
|
+
moduleId: 'lumo-multi-select-combo-box-chip'
|
|
64
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 - 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import '@vaadin/vaadin-lumo-styles/color.js';
|
|
7
|
+
import '@vaadin/vaadin-lumo-styles/font-icons.js';
|
|
8
|
+
import '@vaadin/vaadin-lumo-styles/style.js';
|
|
9
|
+
import '@vaadin/vaadin-lumo-styles/typography.js';
|
|
10
|
+
import { inputFieldShared } from '@vaadin/vaadin-lumo-styles/mixins/input-field-shared.js';
|
|
11
|
+
import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
12
|
+
|
|
13
|
+
const multiSelectComboBox = css`
|
|
14
|
+
:host([has-value]) {
|
|
15
|
+
padding-inline-start: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:host([readonly]) [part~='chip'] {
|
|
19
|
+
opacity: 0.7;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[part~='chip']:not(:last-of-type) {
|
|
23
|
+
margin-inline-end: var(--lumo-space-xs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[part='toggle-button']::before {
|
|
27
|
+
content: var(--lumo-icons-dropdown);
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectComboBox], {
|
|
32
|
+
moduleId: 'lumo-multi-select-combo-box'
|
|
33
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 - 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box-item-styles.js';
|
|
7
|
+
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box-dropdown-styles.js';
|
|
8
|
+
import '@vaadin/input-container/theme/lumo/vaadin-input-container.js';
|
|
9
|
+
import './vaadin-multi-select-combo-box-chip-styles.js';
|
|
10
|
+
import './vaadin-multi-select-combo-box-styles.js';
|
|
11
|
+
import '../../src/vaadin-multi-select-combo-box.js';
|