@everymatrix/general-input 1.31.1 → 1.32.4
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 +21 -0
- package/package.json +3 -2
- package/dist/cjs/checkbox-group-input_10.cjs.entry.js +0 -36077
- package/dist/cjs/general-input.cjs.entry.js +0 -75
- package/dist/cjs/general-input.cjs.js +0 -19
- package/dist/cjs/index-132a0774.js +0 -1327
- package/dist/cjs/index.cjs.js +0 -18
- package/dist/cjs/loader.cjs.js +0 -21
- package/dist/cjs/locale.utils-cef1d3b6.js +0 -128
- package/dist/cjs/toggle-checkbox-input.cjs.entry.js +0 -85
- package/dist/cjs/tooltipIcon-092a795f.js +0 -5
- package/dist/collection/collection-manifest.json +0 -23
- package/dist/collection/components/checkbox-group-input/checkbox-group-input.css +0 -82
- package/dist/collection/components/checkbox-group-input/checkbox-group-input.js +0 -366
- package/dist/collection/components/checkbox-input/checkbox-input.css +0 -68
- package/dist/collection/components/checkbox-input/checkbox-input.js +0 -324
- package/dist/collection/components/date-input/date-input.css +0 -101
- package/dist/collection/components/date-input/date-input.js +0 -396
- package/dist/collection/components/email-input/email-input.css +0 -95
- package/dist/collection/components/email-input/email-input.js +0 -403
- package/dist/collection/components/general-input/general-input.css +0 -4
- package/dist/collection/components/general-input/general-input.js +0 -373
- package/dist/collection/components/number-input/number-input.css +0 -102
- package/dist/collection/components/number-input/number-input.js +0 -368
- package/dist/collection/components/password-input/password-input.css +0 -182
- package/dist/collection/components/password-input/password-input.js +0 -527
- package/dist/collection/components/radio-input/radio-input.css +0 -43
- package/dist/collection/components/radio-input/radio-input.js +0 -297
- package/dist/collection/components/select-input/select-input.css +0 -122
- package/dist/collection/components/select-input/select-input.js +0 -429
- package/dist/collection/components/tel-input/tel-input.css +0 -145
- package/dist/collection/components/tel-input/tel-input.js +0 -440
- package/dist/collection/components/text-input/text-input.css +0 -98
- package/dist/collection/components/text-input/text-input.js +0 -448
- package/dist/collection/components/toggle-checkbox-input/toggle-checkbox-input.css +0 -82
- package/dist/collection/components/toggle-checkbox-input/toggle-checkbox-input.js +0 -324
- package/dist/collection/index.js +0 -17
- package/dist/collection/utils/locale.utils.js +0 -123
- package/dist/collection/utils/tooltipIcon.svg +0 -5
- package/dist/collection/utils/types.js +0 -1
- package/dist/collection/utils/utils.js +0 -5
- package/dist/components/active-mixin.js +0 -975
- package/dist/components/checkbox-group-input.d.ts +0 -11
- package/dist/components/checkbox-group-input.js +0 -6
- package/dist/components/checkbox-group-input2.js +0 -1078
- package/dist/components/checkbox-input.d.ts +0 -11
- package/dist/components/checkbox-input.js +0 -6
- package/dist/components/checkbox-input2.js +0 -129
- package/dist/components/date-input.d.ts +0 -11
- package/dist/components/date-input.js +0 -6
- package/dist/components/date-input2.js +0 -11556
- package/dist/components/email-input.d.ts +0 -11
- package/dist/components/email-input.js +0 -6
- package/dist/components/email-input2.js +0 -171
- package/dist/components/field-mixin.js +0 -12426
- package/dist/components/general-input.d.ts +0 -11
- package/dist/components/general-input.js +0 -6
- package/dist/components/general-input2.js +0 -341
- package/dist/components/index.d.ts +0 -26
- package/dist/components/index.js +0 -18
- package/dist/components/input-field-shared-styles.js +0 -1211
- package/dist/components/number-input.d.ts +0 -11
- package/dist/components/number-input.js +0 -6
- package/dist/components/number-input2.js +0 -158
- package/dist/components/password-input.d.ts +0 -11
- package/dist/components/password-input.js +0 -6
- package/dist/components/password-input2.js +0 -1041
- package/dist/components/radio-input.d.ts +0 -11
- package/dist/components/radio-input.js +0 -6
- package/dist/components/radio-input2.js +0 -114
- package/dist/components/select-input.d.ts +0 -11
- package/dist/components/select-input.js +0 -6
- package/dist/components/select-input2.js +0 -183
- package/dist/components/tel-input.d.ts +0 -11
- package/dist/components/tel-input.js +0 -6
- package/dist/components/tel-input2.js +0 -197
- package/dist/components/text-input.d.ts +0 -11
- package/dist/components/text-input.js +0 -6
- package/dist/components/text-input2.js +0 -199
- package/dist/components/toggle-checkbox-input.d.ts +0 -11
- package/dist/components/toggle-checkbox-input.js +0 -6
- package/dist/components/tooltipIcon.js +0 -127
- package/dist/components/vaadin-button.js +0 -490
- package/dist/components/vaadin-combo-box.js +0 -4512
- package/dist/components/virtual-keyboard-controller.js +0 -2001
- package/dist/esm/checkbox-group-input_10.entry.js +0 -36064
- package/dist/esm/general-input.entry.js +0 -71
- package/dist/esm/general-input.js +0 -17
- package/dist/esm/index-db76d5b5.js +0 -1299
- package/dist/esm/index.js +0 -16
- package/dist/esm/loader.js +0 -17
- package/dist/esm/locale.utils-de759721.js +0 -125
- package/dist/esm/polyfills/core-js.js +0 -11
- package/dist/esm/polyfills/css-shim.js +0 -1
- package/dist/esm/polyfills/dom.js +0 -79
- package/dist/esm/polyfills/es5-html-element.js +0 -1
- package/dist/esm/polyfills/index.js +0 -34
- package/dist/esm/polyfills/system.js +0 -6
- package/dist/esm/toggle-checkbox-input.entry.js +0 -81
- package/dist/esm/tooltipIcon-99c1c7b7.js +0 -3
- package/dist/general-input/general-input.esm.js +0 -1
- package/dist/general-input/index.esm.js +0 -1
- package/dist/general-input/p-10efdf3f.js +0 -1
- package/dist/general-input/p-613c3e00.entry.js +0 -1
- package/dist/general-input/p-660bcdd1.entry.js +0 -1
- package/dist/general-input/p-9b6b0396.entry.js +0 -3646
- package/dist/general-input/p-b408093e.js +0 -1
- package/dist/general-input/p-f4f4ccda.js +0 -1
- package/dist/index.cjs.js +0 -1
- package/dist/index.js +0 -1
- package/dist/stencil.config.js +0 -22
- package/dist/types/Users/sebastian.strulea/Documents/work/widgets-stencil/packages/general-input/.stencil/packages/general-input/stencil.config.d.ts +0 -2
- package/dist/types/components/checkbox-group-input/checkbox-group-input.d.ts +0 -74
- package/dist/types/components/checkbox-input/checkbox-input.d.ts +0 -65
- package/dist/types/components/date-input/date-input.d.ts +0 -84
- package/dist/types/components/email-input/email-input.d.ts +0 -80
- package/dist/types/components/general-input/general-input.d.ts +0 -75
- package/dist/types/components/number-input/number-input.d.ts +0 -74
- package/dist/types/components/password-input/password-input.d.ts +0 -91
- package/dist/types/components/radio-input/radio-input.d.ts +0 -59
- package/dist/types/components/select-input/select-input.d.ts +0 -83
- package/dist/types/components/tel-input/tel-input.d.ts +0 -89
- package/dist/types/components/text-input/text-input.d.ts +0 -84
- package/dist/types/components/toggle-checkbox-input/toggle-checkbox-input.d.ts +0 -67
- package/dist/types/components.d.ts +0 -1268
- package/dist/types/index.d.ts +0 -1
- package/dist/types/stencil-public-runtime.d.ts +0 -1565
- package/dist/types/utils/locale.utils.d.ts +0 -17
- package/dist/types/utils/types.d.ts +0 -87
- package/dist/types/utils/utils.d.ts +0 -1
- package/loader/cdn.js +0 -3
- package/loader/index.cjs.js +0 -3
- package/loader/index.d.ts +0 -12
- package/loader/index.es2017.js +0 -3
- package/loader/index.js +0 -4
- package/loader/package.json +0 -10
|
@@ -1,4512 +0,0 @@
|
|
|
1
|
-
import { i, r as registerStyles, f as defineCustomElement, h as html, g as ThemableMixin, n as DirMixin, P as PolymerElement, m as microTask, R as idlePeriod, U as animationFrame, W as flush, o as Debouncer, X as enqueueDebouncer, t as timeOut, p as generateUniqueId, H as ControllerMixin, V as ValidateMixin, l as FocusMixin, K as KeyboardMixin, I as InputMixin, a as DisabledMixin, N as isElementFocused, c as InputController, e as LabelledInputController, T as TooltipController, E as ElementMixin } from './field-mixin.js';
|
|
2
|
-
import { c as overlay, d as menuOverlayCore, P as PositionMixin, O as OverlayMixin, o as overlayStyles, b as OverlayClassMixin, V as VirtualKeyboardController } from './virtual-keyboard-controller.js';
|
|
3
|
-
import { i as inputFieldShared, e as isSafari, I as InputConstraintsMixin, f as isTouch, c as InputControlMixin, d as inputFieldShared$1 } from './input-field-shared-styles.js';
|
|
4
|
-
|
|
5
|
-
const item = i`
|
|
6
|
-
:host {
|
|
7
|
-
display: flex;
|
|
8
|
-
align-items: center;
|
|
9
|
-
box-sizing: border-box;
|
|
10
|
-
font-family: var(--lumo-font-family);
|
|
11
|
-
font-size: var(--lumo-font-size-m);
|
|
12
|
-
line-height: var(--lumo-line-height-xs);
|
|
13
|
-
padding: 0.5em calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4) 0.5em
|
|
14
|
-
var(--_lumo-list-box-item-padding-left, calc(var(--lumo-border-radius-m) / 4));
|
|
15
|
-
min-height: var(--lumo-size-m);
|
|
16
|
-
outline: none;
|
|
17
|
-
border-radius: var(--lumo-border-radius-m);
|
|
18
|
-
cursor: var(--lumo-clickable-cursor);
|
|
19
|
-
-webkit-font-smoothing: antialiased;
|
|
20
|
-
-moz-osx-font-smoothing: grayscale;
|
|
21
|
-
-webkit-tap-highlight-color: var(--lumo-primary-color-10pct);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/* Checkmark */
|
|
25
|
-
[part='checkmark']::before {
|
|
26
|
-
display: var(--_lumo-item-selected-icon-display, none);
|
|
27
|
-
content: var(--lumo-icons-checkmark);
|
|
28
|
-
font-family: lumo-icons;
|
|
29
|
-
font-size: var(--lumo-icon-size-m);
|
|
30
|
-
line-height: 1;
|
|
31
|
-
font-weight: normal;
|
|
32
|
-
width: 1em;
|
|
33
|
-
height: 1em;
|
|
34
|
-
margin: calc((1 - var(--lumo-line-height-xs)) * var(--lumo-font-size-m) / 2) 0;
|
|
35
|
-
color: var(--lumo-primary-text-color);
|
|
36
|
-
flex: none;
|
|
37
|
-
opacity: 0;
|
|
38
|
-
transition: transform 0.2s cubic-bezier(0.12, 0.32, 0.54, 2), opacity 0.1s;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
:host([selected]) [part='checkmark']::before {
|
|
42
|
-
opacity: 1;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
:host([active]:not([selected])) [part='checkmark']::before {
|
|
46
|
-
transform: scale(0.8);
|
|
47
|
-
opacity: 0;
|
|
48
|
-
transition-duration: 0s;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
[part='content'] {
|
|
52
|
-
flex: auto;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* Disabled */
|
|
56
|
-
:host([disabled]) {
|
|
57
|
-
color: var(--lumo-disabled-text-color);
|
|
58
|
-
cursor: default;
|
|
59
|
-
pointer-events: none;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/* TODO a workaround until we have "focus-follows-mouse". After that, use the hover style for focus-ring as well */
|
|
63
|
-
@media (any-hover: hover) {
|
|
64
|
-
:host(:hover:not([disabled])) {
|
|
65
|
-
background-color: var(--lumo-primary-color-10pct);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
:host([focus-ring]:not([disabled])) {
|
|
69
|
-
box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/* RTL specific styles */
|
|
74
|
-
:host([dir='rtl']) {
|
|
75
|
-
padding-left: calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4);
|
|
76
|
-
padding-right: var(--_lumo-list-box-item-padding-left, calc(var(--lumo-border-radius-m) / 4));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/* Slotted icons */
|
|
80
|
-
:host ::slotted(vaadin-icon) {
|
|
81
|
-
width: var(--lumo-icon-size-m);
|
|
82
|
-
height: var(--lumo-icon-size-m);
|
|
83
|
-
}
|
|
84
|
-
`;
|
|
85
|
-
|
|
86
|
-
registerStyles('vaadin-item', item, { moduleId: 'lumo-item' });
|
|
87
|
-
|
|
88
|
-
const comboBoxItem = i`
|
|
89
|
-
:host {
|
|
90
|
-
transition: background-color 100ms;
|
|
91
|
-
overflow: hidden;
|
|
92
|
-
--_lumo-item-selected-icon-display: block;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
@media (any-hover: hover) {
|
|
96
|
-
:host([focused]:not([disabled])) {
|
|
97
|
-
box-shadow: inset 0 0 0 2px var(--lumo-primary-color-50pct);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
`;
|
|
101
|
-
|
|
102
|
-
registerStyles('vaadin-combo-box-item', [item, comboBoxItem], {
|
|
103
|
-
moduleId: 'lumo-combo-box-item',
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* @license
|
|
108
|
-
* Copyright (c) 2022 - 2023 Vaadin Ltd.
|
|
109
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
110
|
-
*/
|
|
111
|
-
|
|
112
|
-
const loader = i`
|
|
113
|
-
[part~='loader'] {
|
|
114
|
-
box-sizing: border-box;
|
|
115
|
-
width: var(--lumo-icon-size-s);
|
|
116
|
-
height: var(--lumo-icon-size-s);
|
|
117
|
-
border: 2px solid transparent;
|
|
118
|
-
border-color: var(--lumo-primary-color-10pct) var(--lumo-primary-color-10pct) var(--lumo-primary-color)
|
|
119
|
-
var(--lumo-primary-color);
|
|
120
|
-
border-radius: calc(0.5 * var(--lumo-icon-size-s));
|
|
121
|
-
opacity: 0;
|
|
122
|
-
pointer-events: none;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
:host(:not([loading])) [part~='loader'] {
|
|
126
|
-
display: none;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
:host([loading]) [part~='loader'] {
|
|
130
|
-
animation: 1s linear infinite lumo-loader-rotate, 0.3s 0.1s lumo-loader-fade-in both;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
@keyframes lumo-loader-fade-in {
|
|
134
|
-
0% {
|
|
135
|
-
opacity: 0;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
100% {
|
|
139
|
-
opacity: 1;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
@keyframes lumo-loader-rotate {
|
|
144
|
-
0% {
|
|
145
|
-
transform: rotate(0deg);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
100% {
|
|
149
|
-
transform: rotate(360deg);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
`;
|
|
153
|
-
|
|
154
|
-
const comboBoxOverlay = i`
|
|
155
|
-
[part='content'] {
|
|
156
|
-
padding: 0;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/* When items are empty, the spinner needs some room */
|
|
160
|
-
:host(:not([closing])) [part~='content'] {
|
|
161
|
-
min-height: calc(2 * var(--lumo-space-s) + var(--lumo-icon-size-s));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
[part~='overlay'] {
|
|
165
|
-
position: relative;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
:host([top-aligned]) [part~='overlay'] {
|
|
169
|
-
margin-top: var(--lumo-space-xs);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
:host([bottom-aligned]) [part~='overlay'] {
|
|
173
|
-
margin-bottom: var(--lumo-space-xs);
|
|
174
|
-
}
|
|
175
|
-
`;
|
|
176
|
-
|
|
177
|
-
const comboBoxLoader = i`
|
|
178
|
-
[part~='loader'] {
|
|
179
|
-
position: absolute;
|
|
180
|
-
z-index: 1;
|
|
181
|
-
left: var(--lumo-space-s);
|
|
182
|
-
right: var(--lumo-space-s);
|
|
183
|
-
top: var(--lumo-space-s);
|
|
184
|
-
margin-left: auto;
|
|
185
|
-
margin-inline-start: auto;
|
|
186
|
-
margin-inline-end: 0;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
:host([dir='rtl']) [part~='loader'] {
|
|
190
|
-
left: auto;
|
|
191
|
-
margin-left: 0;
|
|
192
|
-
margin-right: auto;
|
|
193
|
-
margin-inline-start: 0;
|
|
194
|
-
margin-inline-end: auto;
|
|
195
|
-
}
|
|
196
|
-
`;
|
|
197
|
-
|
|
198
|
-
registerStyles(
|
|
199
|
-
'vaadin-combo-box-overlay',
|
|
200
|
-
[
|
|
201
|
-
overlay,
|
|
202
|
-
menuOverlayCore,
|
|
203
|
-
comboBoxOverlay,
|
|
204
|
-
loader,
|
|
205
|
-
comboBoxLoader,
|
|
206
|
-
i`
|
|
207
|
-
:host {
|
|
208
|
-
--_vaadin-combo-box-items-container-border-width: var(--lumo-space-xs);
|
|
209
|
-
--_vaadin-combo-box-items-container-border-style: solid;
|
|
210
|
-
}
|
|
211
|
-
`,
|
|
212
|
-
],
|
|
213
|
-
{ moduleId: 'lumo-combo-box-overlay' },
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const comboBox = i`
|
|
217
|
-
:host {
|
|
218
|
-
outline: none;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
[part='toggle-button']::before {
|
|
222
|
-
content: var(--lumo-icons-dropdown);
|
|
223
|
-
}
|
|
224
|
-
`;
|
|
225
|
-
|
|
226
|
-
registerStyles('vaadin-combo-box', [inputFieldShared, comboBox], { moduleId: 'lumo-combo-box' });
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* @license
|
|
230
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
231
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
232
|
-
*/
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* @polymerMixin
|
|
236
|
-
*/
|
|
237
|
-
const ComboBoxItemMixin = (superClass) =>
|
|
238
|
-
class ComboBoxItemMixinClass extends superClass {
|
|
239
|
-
static get properties() {
|
|
240
|
-
return {
|
|
241
|
-
/**
|
|
242
|
-
* The index of the item.
|
|
243
|
-
*/
|
|
244
|
-
index: {
|
|
245
|
-
type: Number,
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* The item to render.
|
|
250
|
-
*/
|
|
251
|
-
item: {
|
|
252
|
-
type: Object,
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* The text to render in the item.
|
|
257
|
-
*/
|
|
258
|
-
label: {
|
|
259
|
-
type: String,
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* True when item is selected.
|
|
264
|
-
*/
|
|
265
|
-
selected: {
|
|
266
|
-
type: Boolean,
|
|
267
|
-
value: false,
|
|
268
|
-
reflectToAttribute: true,
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* True when item is focused.
|
|
273
|
-
*/
|
|
274
|
-
focused: {
|
|
275
|
-
type: Boolean,
|
|
276
|
-
value: false,
|
|
277
|
-
reflectToAttribute: true,
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Custom function for rendering the item content.
|
|
282
|
-
*/
|
|
283
|
-
renderer: {
|
|
284
|
-
type: Function,
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
static get observers() {
|
|
290
|
-
return ['__rendererOrItemChanged(renderer, index, item.*, selected, focused)', '__updateLabel(label, renderer)'];
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
static get observedAttributes() {
|
|
294
|
-
return [...super.observedAttributes, 'hidden'];
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
298
|
-
if (name === 'hidden' && newValue !== null) {
|
|
299
|
-
// The element is being hidden (by virtualizer). Mark one of the __rendererOrItemChanged
|
|
300
|
-
// dependencies as undefined to make sure it's called when the element is shown again
|
|
301
|
-
// and assigned properties with possibly identical values as before hiding.
|
|
302
|
-
this.index = undefined;
|
|
303
|
-
} else {
|
|
304
|
-
super.attributeChangedCallback(name, oldValue, newValue);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** @protected */
|
|
309
|
-
connectedCallback() {
|
|
310
|
-
super.connectedCallback();
|
|
311
|
-
|
|
312
|
-
this._owner = this.parentNode.owner;
|
|
313
|
-
|
|
314
|
-
const hostDir = this._owner.getAttribute('dir');
|
|
315
|
-
if (hostDir) {
|
|
316
|
-
this.setAttribute('dir', hostDir);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Requests an update for the content of the item.
|
|
322
|
-
* While performing the update, it invokes the renderer passed in the `renderer` property.
|
|
323
|
-
*
|
|
324
|
-
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
|
|
325
|
-
*/
|
|
326
|
-
requestContentUpdate() {
|
|
327
|
-
if (!this.renderer) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const model = {
|
|
332
|
-
index: this.index,
|
|
333
|
-
item: this.item,
|
|
334
|
-
focused: this.focused,
|
|
335
|
-
selected: this.selected,
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
this.renderer(this, this._owner, model);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** @private */
|
|
342
|
-
__rendererOrItemChanged(renderer, index, item) {
|
|
343
|
-
if (item === undefined || index === undefined) {
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (this._oldRenderer !== renderer) {
|
|
348
|
-
this.innerHTML = '';
|
|
349
|
-
// Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
|
|
350
|
-
// When clearing the rendered content, this part needs to be manually disposed of.
|
|
351
|
-
// Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
|
|
352
|
-
delete this._$litPart$;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (renderer) {
|
|
356
|
-
this._oldRenderer = renderer;
|
|
357
|
-
this.requestContentUpdate();
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/** @private */
|
|
362
|
-
__updateLabel(label, renderer) {
|
|
363
|
-
if (renderer) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
this.textContent = label;
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* @license
|
|
373
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
374
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
375
|
-
*/
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* An item element used by the `<vaadin-combo-box>` dropdown.
|
|
379
|
-
*
|
|
380
|
-
* ### Styling
|
|
381
|
-
*
|
|
382
|
-
* The following shadow DOM parts are available for styling:
|
|
383
|
-
*
|
|
384
|
-
* Part name | Description
|
|
385
|
-
* ------------|--------------
|
|
386
|
-
* `checkmark` | The graphical checkmark shown for a selected item
|
|
387
|
-
* `content` | The element that wraps the item content
|
|
388
|
-
*
|
|
389
|
-
* The following state attributes are exposed for styling:
|
|
390
|
-
*
|
|
391
|
-
* Attribute | Description
|
|
392
|
-
* -------------|-------------
|
|
393
|
-
* `selected` | Set when the item is selected
|
|
394
|
-
* `focused` | Set when the item is focused
|
|
395
|
-
*
|
|
396
|
-
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
|
|
397
|
-
*
|
|
398
|
-
* @customElement
|
|
399
|
-
* @mixes ComboBoxItemMixin
|
|
400
|
-
* @mixes ThemableMixin
|
|
401
|
-
* @mixes DirMixin
|
|
402
|
-
* @private
|
|
403
|
-
*/
|
|
404
|
-
class ComboBoxItem extends ComboBoxItemMixin(ThemableMixin(DirMixin(PolymerElement))) {
|
|
405
|
-
static get template() {
|
|
406
|
-
return html`
|
|
407
|
-
<style>
|
|
408
|
-
:host {
|
|
409
|
-
display: block;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
:host([hidden]) {
|
|
413
|
-
display: none;
|
|
414
|
-
}
|
|
415
|
-
</style>
|
|
416
|
-
<span part="checkmark" aria-hidden="true"></span>
|
|
417
|
-
<div part="content">
|
|
418
|
-
<slot></slot>
|
|
419
|
-
</div>
|
|
420
|
-
`;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
static get is() {
|
|
424
|
-
return 'vaadin-combo-box-item';
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
defineCustomElement(ComboBoxItem);
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* @license
|
|
432
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
433
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
434
|
-
*/
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* @polymerMixin
|
|
438
|
-
* @mixes PositionMixin
|
|
439
|
-
*/
|
|
440
|
-
const ComboBoxOverlayMixin = (superClass) =>
|
|
441
|
-
class ComboBoxOverlayMixin extends PositionMixin(superClass) {
|
|
442
|
-
static get observers() {
|
|
443
|
-
return ['_setOverlayWidth(positionTarget, opened)'];
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
constructor() {
|
|
447
|
-
super();
|
|
448
|
-
|
|
449
|
-
this.requiredVerticalSpace = 200;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/** @protected */
|
|
453
|
-
connectedCallback() {
|
|
454
|
-
super.connectedCallback();
|
|
455
|
-
|
|
456
|
-
const comboBox = this._comboBox;
|
|
457
|
-
|
|
458
|
-
const hostDir = comboBox && comboBox.getAttribute('dir');
|
|
459
|
-
if (hostDir) {
|
|
460
|
-
this.setAttribute('dir', hostDir);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Override method inherited from `Overlay`
|
|
466
|
-
* to not close on position target click.
|
|
467
|
-
*
|
|
468
|
-
* @param {Event} event
|
|
469
|
-
* @return {boolean}
|
|
470
|
-
* @protected
|
|
471
|
-
*/
|
|
472
|
-
_shouldCloseOnOutsideClick(event) {
|
|
473
|
-
const eventPath = event.composedPath();
|
|
474
|
-
return !eventPath.includes(this.positionTarget) && !eventPath.includes(this);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/** @private */
|
|
478
|
-
_setOverlayWidth(positionTarget, opened) {
|
|
479
|
-
if (positionTarget && opened) {
|
|
480
|
-
const propPrefix = this.localName;
|
|
481
|
-
this.style.setProperty(`--_${propPrefix}-default-width`, `${positionTarget.clientWidth}px`);
|
|
482
|
-
|
|
483
|
-
const customWidth = getComputedStyle(this._comboBox).getPropertyValue(`--${propPrefix}-width`);
|
|
484
|
-
|
|
485
|
-
if (customWidth === '') {
|
|
486
|
-
this.style.removeProperty(`--${propPrefix}-width`);
|
|
487
|
-
} else {
|
|
488
|
-
this.style.setProperty(`--${propPrefix}-width`, customWidth);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
this._updatePosition();
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* @license
|
|
498
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
499
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
500
|
-
*/
|
|
501
|
-
|
|
502
|
-
const comboBoxOverlayStyles = i`
|
|
503
|
-
#overlay {
|
|
504
|
-
width: var(--vaadin-combo-box-overlay-width, var(--_vaadin-combo-box-overlay-default-width, auto));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
[part='content'] {
|
|
508
|
-
display: flex;
|
|
509
|
-
flex-direction: column;
|
|
510
|
-
height: 100%;
|
|
511
|
-
}
|
|
512
|
-
`;
|
|
513
|
-
|
|
514
|
-
registerStyles('vaadin-combo-box-overlay', [overlayStyles, comboBoxOverlayStyles], {
|
|
515
|
-
moduleId: 'vaadin-combo-box-overlay-styles',
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* An element used internally by `<vaadin-combo-box>`. Not intended to be used separately.
|
|
520
|
-
*
|
|
521
|
-
* @customElement
|
|
522
|
-
* @extends HTMLElement
|
|
523
|
-
* @mixes ComboBoxOverlayMixin
|
|
524
|
-
* @mixes DirMixin
|
|
525
|
-
* @mixes OverlayMixin
|
|
526
|
-
* @mixes ThemableMixin
|
|
527
|
-
* @private
|
|
528
|
-
*/
|
|
529
|
-
class ComboBoxOverlay extends ComboBoxOverlayMixin(OverlayMixin(DirMixin(ThemableMixin(PolymerElement)))) {
|
|
530
|
-
static get is() {
|
|
531
|
-
return 'vaadin-combo-box-overlay';
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
static get template() {
|
|
535
|
-
return html`
|
|
536
|
-
<div id="backdrop" part="backdrop" hidden></div>
|
|
537
|
-
<div part="overlay" id="overlay">
|
|
538
|
-
<div part="loader"></div>
|
|
539
|
-
<div part="content" id="content"><slot></slot></div>
|
|
540
|
-
</div>
|
|
541
|
-
`;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
defineCustomElement(ComboBoxOverlay);
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* @license
|
|
549
|
-
* Copyright (c) 2023 Vaadin Ltd.
|
|
550
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
551
|
-
*/
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Convenience method for reading a value from a path.
|
|
555
|
-
*
|
|
556
|
-
* @param {string} path
|
|
557
|
-
* @param {object} object
|
|
558
|
-
*/
|
|
559
|
-
function get(path, object) {
|
|
560
|
-
return path.split('.').reduce((obj, property) => (obj ? obj[property] : undefined), object);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* @license
|
|
565
|
-
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
|
566
|
-
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
567
|
-
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
568
|
-
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
569
|
-
* Code distributed by Google as part of the polymer project is also
|
|
570
|
-
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
571
|
-
*/
|
|
572
|
-
|
|
573
|
-
const IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/u);
|
|
574
|
-
const IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
|
|
575
|
-
const DEFAULT_PHYSICAL_COUNT = 3;
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* DO NOT EDIT THIS FILE!
|
|
579
|
-
*
|
|
580
|
-
* This file includes the iron-list scrolling engine copied from
|
|
581
|
-
* https://github.com/PolymerElements/iron-list/blob/master/iron-list.js
|
|
582
|
-
*
|
|
583
|
-
* If something in the scrolling engine needs to be changed
|
|
584
|
-
* for the virtualizer's purposes, override a function
|
|
585
|
-
* in virtualizer-iron-list-adapter.js instead of changing it here.
|
|
586
|
-
* If a function on this file is no longer needed, the code can be safely deleted.
|
|
587
|
-
*
|
|
588
|
-
* This will allow us to keep the iron-list code here as close to
|
|
589
|
-
* the original as possible.
|
|
590
|
-
*/
|
|
591
|
-
const ironList = {
|
|
592
|
-
/**
|
|
593
|
-
* The ratio of hidden tiles that should remain in the scroll direction.
|
|
594
|
-
* Recommended value ~0.5, so it will distribute tiles evenly in both
|
|
595
|
-
* directions.
|
|
596
|
-
*/
|
|
597
|
-
_ratio: 0.5,
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* The padding-top value for the list.
|
|
601
|
-
*/
|
|
602
|
-
_scrollerPaddingTop: 0,
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* This value is a cached value of `scrollTop` from the last `scroll` event.
|
|
606
|
-
*/
|
|
607
|
-
_scrollPosition: 0,
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* The sum of the heights of all the tiles in the DOM.
|
|
611
|
-
*/
|
|
612
|
-
_physicalSize: 0,
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* The average `offsetHeight` of the tiles observed till now.
|
|
616
|
-
*/
|
|
617
|
-
_physicalAverage: 0,
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* The number of tiles which `offsetHeight` > 0 observed until now.
|
|
621
|
-
*/
|
|
622
|
-
_physicalAverageCount: 0,
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* The Y position of the item rendered in the `_physicalStart`
|
|
626
|
-
* tile relative to the scrolling list.
|
|
627
|
-
*/
|
|
628
|
-
_physicalTop: 0,
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* The number of items in the list.
|
|
632
|
-
*/
|
|
633
|
-
_virtualCount: 0,
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* The estimated scroll height based on `_physicalAverage`
|
|
637
|
-
*/
|
|
638
|
-
_estScrollHeight: 0,
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* The scroll height of the dom node
|
|
642
|
-
*/
|
|
643
|
-
_scrollHeight: 0,
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* The height of the list. This is referred as the viewport in the context of
|
|
647
|
-
* list.
|
|
648
|
-
*/
|
|
649
|
-
_viewportHeight: 0,
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* The width of the list. This is referred as the viewport in the context of
|
|
653
|
-
* list.
|
|
654
|
-
*/
|
|
655
|
-
_viewportWidth: 0,
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* An array of DOM nodes that are currently in the tree
|
|
659
|
-
* @type {?Array<!HTMLElement>}
|
|
660
|
-
*/
|
|
661
|
-
_physicalItems: null,
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* An array of heights for each item in `_physicalItems`
|
|
665
|
-
* @type {?Array<number>}
|
|
666
|
-
*/
|
|
667
|
-
_physicalSizes: null,
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* A cached value for the first visible index.
|
|
671
|
-
* See `firstVisibleIndex`
|
|
672
|
-
* @type {?number}
|
|
673
|
-
*/
|
|
674
|
-
_firstVisibleIndexVal: null,
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* A cached value for the last visible index.
|
|
678
|
-
* See `lastVisibleIndex`
|
|
679
|
-
* @type {?number}
|
|
680
|
-
*/
|
|
681
|
-
_lastVisibleIndexVal: null,
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* The max number of pages to render. One page is equivalent to the height of
|
|
685
|
-
* the list.
|
|
686
|
-
*/
|
|
687
|
-
_maxPages: 2,
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* The cost of stamping a template in ms.
|
|
691
|
-
*/
|
|
692
|
-
_templateCost: 0,
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* The bottom of the physical content.
|
|
696
|
-
*/
|
|
697
|
-
get _physicalBottom() {
|
|
698
|
-
return this._physicalTop + this._physicalSize;
|
|
699
|
-
},
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* The bottom of the scroll.
|
|
703
|
-
*/
|
|
704
|
-
get _scrollBottom() {
|
|
705
|
-
return this._scrollPosition + this._viewportHeight;
|
|
706
|
-
},
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* The n-th item rendered in the last physical item.
|
|
710
|
-
*/
|
|
711
|
-
get _virtualEnd() {
|
|
712
|
-
return this._virtualStart + this._physicalCount - 1;
|
|
713
|
-
},
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* The height of the physical content that isn't on the screen.
|
|
717
|
-
*/
|
|
718
|
-
get _hiddenContentSize() {
|
|
719
|
-
return this._physicalSize - this._viewportHeight;
|
|
720
|
-
},
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* The maximum scroll top value.
|
|
724
|
-
*/
|
|
725
|
-
get _maxScrollTop() {
|
|
726
|
-
return this._estScrollHeight - this._viewportHeight + this._scrollOffset;
|
|
727
|
-
},
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* The largest n-th value for an item such that it can be rendered in
|
|
731
|
-
* `_physicalStart`.
|
|
732
|
-
*/
|
|
733
|
-
get _maxVirtualStart() {
|
|
734
|
-
const virtualCount = this._virtualCount;
|
|
735
|
-
return Math.max(0, virtualCount - this._physicalCount);
|
|
736
|
-
},
|
|
737
|
-
|
|
738
|
-
get _virtualStart() {
|
|
739
|
-
return this._virtualStartVal || 0;
|
|
740
|
-
},
|
|
741
|
-
|
|
742
|
-
set _virtualStart(val) {
|
|
743
|
-
val = this._clamp(val, 0, this._maxVirtualStart);
|
|
744
|
-
this._virtualStartVal = val;
|
|
745
|
-
},
|
|
746
|
-
|
|
747
|
-
get _physicalStart() {
|
|
748
|
-
return this._physicalStartVal || 0;
|
|
749
|
-
},
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* The k-th tile that is at the top of the scrolling list.
|
|
753
|
-
*/
|
|
754
|
-
set _physicalStart(val) {
|
|
755
|
-
val %= this._physicalCount;
|
|
756
|
-
if (val < 0) {
|
|
757
|
-
val = this._physicalCount + val;
|
|
758
|
-
}
|
|
759
|
-
this._physicalStartVal = val;
|
|
760
|
-
},
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* The k-th tile that is at the bottom of the scrolling list.
|
|
764
|
-
*/
|
|
765
|
-
get _physicalEnd() {
|
|
766
|
-
return (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
|
767
|
-
},
|
|
768
|
-
|
|
769
|
-
get _physicalCount() {
|
|
770
|
-
return this._physicalCountVal || 0;
|
|
771
|
-
},
|
|
772
|
-
|
|
773
|
-
set _physicalCount(val) {
|
|
774
|
-
this._physicalCountVal = val;
|
|
775
|
-
},
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* An optimal physical size such that we will have enough physical items
|
|
779
|
-
* to fill up the viewport and recycle when the user scrolls.
|
|
780
|
-
*
|
|
781
|
-
* This default value assumes that we will at least have the equivalent
|
|
782
|
-
* to a viewport of physical items above and below the user's viewport.
|
|
783
|
-
*/
|
|
784
|
-
get _optPhysicalSize() {
|
|
785
|
-
return this._viewportHeight === 0 ? Infinity : this._viewportHeight * this._maxPages;
|
|
786
|
-
},
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* True if the current list is visible.
|
|
790
|
-
*/
|
|
791
|
-
get _isVisible() {
|
|
792
|
-
return Boolean(this.offsetWidth || this.offsetHeight);
|
|
793
|
-
},
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Gets the index of the first visible item in the viewport.
|
|
797
|
-
*
|
|
798
|
-
* @type {number}
|
|
799
|
-
*/
|
|
800
|
-
get firstVisibleIndex() {
|
|
801
|
-
let idx = this._firstVisibleIndexVal;
|
|
802
|
-
if (idx == null) {
|
|
803
|
-
let physicalOffset = this._physicalTop + this._scrollOffset;
|
|
804
|
-
|
|
805
|
-
idx =
|
|
806
|
-
this._iterateItems((pidx, vidx) => {
|
|
807
|
-
physicalOffset += this._getPhysicalSizeIncrement(pidx);
|
|
808
|
-
|
|
809
|
-
if (physicalOffset > this._scrollPosition) {
|
|
810
|
-
return vidx;
|
|
811
|
-
}
|
|
812
|
-
}) || 0;
|
|
813
|
-
this._firstVisibleIndexVal = idx;
|
|
814
|
-
}
|
|
815
|
-
return idx;
|
|
816
|
-
},
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Gets the index of the last visible item in the viewport.
|
|
820
|
-
*
|
|
821
|
-
* @type {number}
|
|
822
|
-
*/
|
|
823
|
-
get lastVisibleIndex() {
|
|
824
|
-
let idx = this._lastVisibleIndexVal;
|
|
825
|
-
if (idx == null) {
|
|
826
|
-
let physicalOffset = this._physicalTop + this._scrollOffset;
|
|
827
|
-
this._iterateItems((pidx, vidx) => {
|
|
828
|
-
if (physicalOffset < this._scrollBottom) {
|
|
829
|
-
idx = vidx;
|
|
830
|
-
}
|
|
831
|
-
physicalOffset += this._getPhysicalSizeIncrement(pidx);
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
this._lastVisibleIndexVal = idx;
|
|
835
|
-
}
|
|
836
|
-
return idx;
|
|
837
|
-
},
|
|
838
|
-
|
|
839
|
-
get _scrollOffset() {
|
|
840
|
-
return this._scrollerPaddingTop + this.scrollOffset;
|
|
841
|
-
},
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Recycles the physical items when needed.
|
|
845
|
-
*/
|
|
846
|
-
_scrollHandler() {
|
|
847
|
-
const scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
|
|
848
|
-
let delta = scrollTop - this._scrollPosition;
|
|
849
|
-
const isScrollingDown = delta >= 0;
|
|
850
|
-
// Track the current scroll position.
|
|
851
|
-
this._scrollPosition = scrollTop;
|
|
852
|
-
// Clear indexes for first and last visible indexes.
|
|
853
|
-
this._firstVisibleIndexVal = null;
|
|
854
|
-
this._lastVisibleIndexVal = null;
|
|
855
|
-
// Random access.
|
|
856
|
-
if (Math.abs(delta) > this._physicalSize && this._physicalSize > 0) {
|
|
857
|
-
delta -= this._scrollOffset;
|
|
858
|
-
const idxAdjustment = Math.round(delta / this._physicalAverage);
|
|
859
|
-
this._virtualStart += idxAdjustment;
|
|
860
|
-
this._physicalStart += idxAdjustment;
|
|
861
|
-
// Estimate new physical offset based on the virtual start index.
|
|
862
|
-
// adjusts the physical start position to stay in sync with the clamped
|
|
863
|
-
// virtual start index. It's critical not to let this value be
|
|
864
|
-
// more than the scroll position however, since that would result in
|
|
865
|
-
// the physical items not covering the viewport, and leading to
|
|
866
|
-
// _increasePoolIfNeeded to run away creating items to try to fill it.
|
|
867
|
-
this._physicalTop = Math.min(Math.floor(this._virtualStart) * this._physicalAverage, this._scrollPosition);
|
|
868
|
-
this._update();
|
|
869
|
-
} else if (this._physicalCount > 0) {
|
|
870
|
-
const reusables = this._getReusables(isScrollingDown);
|
|
871
|
-
if (isScrollingDown) {
|
|
872
|
-
this._physicalTop = reusables.physicalTop;
|
|
873
|
-
this._virtualStart += reusables.indexes.length;
|
|
874
|
-
this._physicalStart += reusables.indexes.length;
|
|
875
|
-
} else {
|
|
876
|
-
this._virtualStart -= reusables.indexes.length;
|
|
877
|
-
this._physicalStart -= reusables.indexes.length;
|
|
878
|
-
}
|
|
879
|
-
this._update(reusables.indexes, isScrollingDown ? null : reusables.indexes);
|
|
880
|
-
this._debounce('_increasePoolIfNeeded', this._increasePoolIfNeeded.bind(this, 0), microTask);
|
|
881
|
-
}
|
|
882
|
-
},
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Returns an object that contains the indexes of the physical items
|
|
886
|
-
* that might be reused and the physicalTop.
|
|
887
|
-
*
|
|
888
|
-
* @param {boolean} fromTop If the potential reusable items are above the scrolling region.
|
|
889
|
-
*/
|
|
890
|
-
_getReusables(fromTop) {
|
|
891
|
-
let ith, offsetContent, physicalItemHeight;
|
|
892
|
-
const idxs = [];
|
|
893
|
-
const protectedOffsetContent = this._hiddenContentSize * this._ratio;
|
|
894
|
-
const virtualStart = this._virtualStart;
|
|
895
|
-
const virtualEnd = this._virtualEnd;
|
|
896
|
-
const physicalCount = this._physicalCount;
|
|
897
|
-
let top = this._physicalTop + this._scrollOffset;
|
|
898
|
-
const bottom = this._physicalBottom + this._scrollOffset;
|
|
899
|
-
// This may be called outside of a scrollHandler, so use last cached position
|
|
900
|
-
const scrollTop = this._scrollPosition;
|
|
901
|
-
const scrollBottom = this._scrollBottom;
|
|
902
|
-
|
|
903
|
-
if (fromTop) {
|
|
904
|
-
ith = this._physicalStart;
|
|
905
|
-
offsetContent = scrollTop - top;
|
|
906
|
-
} else {
|
|
907
|
-
ith = this._physicalEnd;
|
|
908
|
-
offsetContent = bottom - scrollBottom;
|
|
909
|
-
}
|
|
910
|
-
// eslint-disable-next-line no-constant-condition
|
|
911
|
-
while (true) {
|
|
912
|
-
physicalItemHeight = this._getPhysicalSizeIncrement(ith);
|
|
913
|
-
offsetContent -= physicalItemHeight;
|
|
914
|
-
if (idxs.length >= physicalCount || offsetContent <= protectedOffsetContent) {
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
917
|
-
if (fromTop) {
|
|
918
|
-
// Check that index is within the valid range.
|
|
919
|
-
if (virtualEnd + idxs.length + 1 >= this._virtualCount) {
|
|
920
|
-
break;
|
|
921
|
-
}
|
|
922
|
-
// Check that the index is not visible.
|
|
923
|
-
if (top + physicalItemHeight >= scrollTop - this._scrollOffset) {
|
|
924
|
-
break;
|
|
925
|
-
}
|
|
926
|
-
idxs.push(ith);
|
|
927
|
-
top += physicalItemHeight;
|
|
928
|
-
ith = (ith + 1) % physicalCount;
|
|
929
|
-
} else {
|
|
930
|
-
// Check that index is within the valid range.
|
|
931
|
-
if (virtualStart - idxs.length <= 0) {
|
|
932
|
-
break;
|
|
933
|
-
}
|
|
934
|
-
// Check that the index is not visible.
|
|
935
|
-
if (top + this._physicalSize - physicalItemHeight <= scrollBottom) {
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
938
|
-
idxs.push(ith);
|
|
939
|
-
top -= physicalItemHeight;
|
|
940
|
-
ith = ith === 0 ? physicalCount - 1 : ith - 1;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
return { indexes: idxs, physicalTop: top - this._scrollOffset };
|
|
944
|
-
},
|
|
945
|
-
|
|
946
|
-
/**
|
|
947
|
-
* Update the list of items, starting from the `_virtualStart` item.
|
|
948
|
-
* @param {!Array<number>=} itemSet
|
|
949
|
-
* @param {!Array<number>=} movingUp
|
|
950
|
-
*/
|
|
951
|
-
_update(itemSet, movingUp) {
|
|
952
|
-
if ((itemSet && itemSet.length === 0) || this._physicalCount === 0) {
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
this._assignModels(itemSet);
|
|
956
|
-
this._updateMetrics(itemSet);
|
|
957
|
-
// Adjust offset after measuring.
|
|
958
|
-
if (movingUp) {
|
|
959
|
-
while (movingUp.length) {
|
|
960
|
-
const idx = movingUp.pop();
|
|
961
|
-
this._physicalTop -= this._getPhysicalSizeIncrement(idx);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
this._positionItems();
|
|
965
|
-
this._updateScrollerSize();
|
|
966
|
-
},
|
|
967
|
-
|
|
968
|
-
_isClientFull() {
|
|
969
|
-
return (
|
|
970
|
-
this._scrollBottom !== 0 &&
|
|
971
|
-
this._physicalBottom - 1 >= this._scrollBottom &&
|
|
972
|
-
this._physicalTop <= this._scrollPosition
|
|
973
|
-
);
|
|
974
|
-
},
|
|
975
|
-
|
|
976
|
-
/**
|
|
977
|
-
* Increases the pool size.
|
|
978
|
-
*/
|
|
979
|
-
_increasePoolIfNeeded(count) {
|
|
980
|
-
const nextPhysicalCount = this._clamp(
|
|
981
|
-
this._physicalCount + count,
|
|
982
|
-
DEFAULT_PHYSICAL_COUNT,
|
|
983
|
-
this._virtualCount - this._virtualStart,
|
|
984
|
-
);
|
|
985
|
-
const delta = nextPhysicalCount - this._physicalCount;
|
|
986
|
-
let nextIncrease = Math.round(this._physicalCount * 0.5);
|
|
987
|
-
|
|
988
|
-
if (delta < 0) {
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
if (delta > 0) {
|
|
992
|
-
const ts = window.performance.now();
|
|
993
|
-
// Concat arrays in place.
|
|
994
|
-
[].push.apply(this._physicalItems, this._createPool(delta));
|
|
995
|
-
// Push 0s into physicalSizes. Can't use Array.fill because IE11 doesn't
|
|
996
|
-
// support it.
|
|
997
|
-
for (let i = 0; i < delta; i++) {
|
|
998
|
-
this._physicalSizes.push(0);
|
|
999
|
-
}
|
|
1000
|
-
this._physicalCount += delta;
|
|
1001
|
-
// Update the physical start if it needs to preserve the model of the
|
|
1002
|
-
// focused item. In this situation, the focused item is currently rendered
|
|
1003
|
-
// and its model would have changed after increasing the pool if the
|
|
1004
|
-
// physical start remained unchanged.
|
|
1005
|
-
if (
|
|
1006
|
-
this._physicalStart > this._physicalEnd &&
|
|
1007
|
-
this._isIndexRendered(this._focusedVirtualIndex) &&
|
|
1008
|
-
this._getPhysicalIndex(this._focusedVirtualIndex) < this._physicalEnd
|
|
1009
|
-
) {
|
|
1010
|
-
this._physicalStart += delta;
|
|
1011
|
-
}
|
|
1012
|
-
this._update();
|
|
1013
|
-
this._templateCost = (window.performance.now() - ts) / delta;
|
|
1014
|
-
nextIncrease = Math.round(this._physicalCount * 0.5);
|
|
1015
|
-
}
|
|
1016
|
-
if (this._virtualEnd >= this._virtualCount - 1 || nextIncrease === 0) ; else if (!this._isClientFull()) {
|
|
1017
|
-
this._debounce('_increasePoolIfNeeded', this._increasePoolIfNeeded.bind(this, nextIncrease), microTask);
|
|
1018
|
-
} else if (this._physicalSize < this._optPhysicalSize) {
|
|
1019
|
-
// Yield and increase the pool during idle time until the physical size is
|
|
1020
|
-
// optimal.
|
|
1021
|
-
this._debounce(
|
|
1022
|
-
'_increasePoolIfNeeded',
|
|
1023
|
-
this._increasePoolIfNeeded.bind(this, this._clamp(Math.round(50 / this._templateCost), 1, nextIncrease)),
|
|
1024
|
-
idlePeriod,
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
},
|
|
1028
|
-
|
|
1029
|
-
/**
|
|
1030
|
-
* Renders the a new list.
|
|
1031
|
-
*/
|
|
1032
|
-
_render() {
|
|
1033
|
-
if (!this.isAttached || !this._isVisible) {
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
if (this._physicalCount !== 0) {
|
|
1037
|
-
const reusables = this._getReusables(true);
|
|
1038
|
-
this._physicalTop = reusables.physicalTop;
|
|
1039
|
-
this._virtualStart += reusables.indexes.length;
|
|
1040
|
-
this._physicalStart += reusables.indexes.length;
|
|
1041
|
-
this._update(reusables.indexes);
|
|
1042
|
-
this._update();
|
|
1043
|
-
this._increasePoolIfNeeded(0);
|
|
1044
|
-
} else if (this._virtualCount > 0) {
|
|
1045
|
-
// Initial render
|
|
1046
|
-
this.updateViewportBoundaries();
|
|
1047
|
-
this._increasePoolIfNeeded(DEFAULT_PHYSICAL_COUNT);
|
|
1048
|
-
}
|
|
1049
|
-
},
|
|
1050
|
-
|
|
1051
|
-
/**
|
|
1052
|
-
* Called when the items have changed. That is, reassignments
|
|
1053
|
-
* to `items`, splices or updates to a single item.
|
|
1054
|
-
*/
|
|
1055
|
-
_itemsChanged(change) {
|
|
1056
|
-
if (change.path === 'items') {
|
|
1057
|
-
this._virtualStart = 0;
|
|
1058
|
-
this._physicalTop = 0;
|
|
1059
|
-
this._virtualCount = this.items ? this.items.length : 0;
|
|
1060
|
-
this._physicalIndexForKey = {};
|
|
1061
|
-
this._firstVisibleIndexVal = null;
|
|
1062
|
-
this._lastVisibleIndexVal = null;
|
|
1063
|
-
if (!this._physicalItems) {
|
|
1064
|
-
this._physicalItems = [];
|
|
1065
|
-
}
|
|
1066
|
-
if (!this._physicalSizes) {
|
|
1067
|
-
this._physicalSizes = [];
|
|
1068
|
-
}
|
|
1069
|
-
this._physicalStart = 0;
|
|
1070
|
-
if (this._scrollTop > this._scrollOffset) {
|
|
1071
|
-
this._resetScrollPosition(0);
|
|
1072
|
-
}
|
|
1073
|
-
this._debounce('_render', this._render, animationFrame);
|
|
1074
|
-
}
|
|
1075
|
-
},
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* Executes a provided function per every physical index in `itemSet`
|
|
1079
|
-
* `itemSet` default value is equivalent to the entire set of physical
|
|
1080
|
-
* indexes.
|
|
1081
|
-
*
|
|
1082
|
-
* @param {!function(number, number)} fn
|
|
1083
|
-
* @param {!Array<number>=} itemSet
|
|
1084
|
-
*/
|
|
1085
|
-
_iterateItems(fn, itemSet) {
|
|
1086
|
-
let pidx, vidx, rtn, i;
|
|
1087
|
-
|
|
1088
|
-
if (arguments.length === 2 && itemSet) {
|
|
1089
|
-
for (i = 0; i < itemSet.length; i++) {
|
|
1090
|
-
pidx = itemSet[i];
|
|
1091
|
-
vidx = this._computeVidx(pidx);
|
|
1092
|
-
if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
|
1093
|
-
return rtn;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
} else {
|
|
1097
|
-
pidx = this._physicalStart;
|
|
1098
|
-
vidx = this._virtualStart;
|
|
1099
|
-
for (; pidx < this._physicalCount; pidx++, vidx++) {
|
|
1100
|
-
if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
|
1101
|
-
return rtn;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
|
|
1105
|
-
if ((rtn = fn.call(this, pidx, vidx)) != null) {
|
|
1106
|
-
return rtn;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
},
|
|
1111
|
-
|
|
1112
|
-
/**
|
|
1113
|
-
* Returns the virtual index for a given physical index
|
|
1114
|
-
*
|
|
1115
|
-
* @param {number} pidx Physical index
|
|
1116
|
-
* @return {number}
|
|
1117
|
-
*/
|
|
1118
|
-
_computeVidx(pidx) {
|
|
1119
|
-
if (pidx >= this._physicalStart) {
|
|
1120
|
-
return this._virtualStart + (pidx - this._physicalStart);
|
|
1121
|
-
}
|
|
1122
|
-
return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
|
|
1123
|
-
},
|
|
1124
|
-
|
|
1125
|
-
/**
|
|
1126
|
-
* Updates the position of the physical items.
|
|
1127
|
-
*/
|
|
1128
|
-
_positionItems() {
|
|
1129
|
-
this._adjustScrollPosition();
|
|
1130
|
-
|
|
1131
|
-
let y = this._physicalTop;
|
|
1132
|
-
|
|
1133
|
-
this._iterateItems((pidx) => {
|
|
1134
|
-
this.translate3d(0, `${y}px`, 0, this._physicalItems[pidx]);
|
|
1135
|
-
y += this._physicalSizes[pidx];
|
|
1136
|
-
});
|
|
1137
|
-
},
|
|
1138
|
-
|
|
1139
|
-
_getPhysicalSizeIncrement(pidx) {
|
|
1140
|
-
return this._physicalSizes[pidx];
|
|
1141
|
-
},
|
|
1142
|
-
|
|
1143
|
-
/**
|
|
1144
|
-
* Adjusts the scroll position when it was overestimated.
|
|
1145
|
-
*/
|
|
1146
|
-
_adjustScrollPosition() {
|
|
1147
|
-
const deltaHeight =
|
|
1148
|
-
this._virtualStart === 0 ? this._physicalTop : Math.min(this._scrollPosition + this._physicalTop, 0);
|
|
1149
|
-
// Note: the delta can be positive or negative.
|
|
1150
|
-
if (deltaHeight !== 0) {
|
|
1151
|
-
this._physicalTop -= deltaHeight;
|
|
1152
|
-
// This may be called outside of a scrollHandler, so use last cached position
|
|
1153
|
-
const scrollTop = this._scrollPosition;
|
|
1154
|
-
// Juking scroll position during interial scrolling on iOS is no bueno
|
|
1155
|
-
if (!IOS_TOUCH_SCROLLING && scrollTop > 0) {
|
|
1156
|
-
this._resetScrollPosition(scrollTop - deltaHeight);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
},
|
|
1160
|
-
|
|
1161
|
-
/**
|
|
1162
|
-
* Sets the position of the scroll.
|
|
1163
|
-
*/
|
|
1164
|
-
_resetScrollPosition(pos) {
|
|
1165
|
-
if (this.scrollTarget && pos >= 0) {
|
|
1166
|
-
this._scrollTop = pos;
|
|
1167
|
-
this._scrollPosition = this._scrollTop;
|
|
1168
|
-
}
|
|
1169
|
-
},
|
|
1170
|
-
|
|
1171
|
-
/**
|
|
1172
|
-
* Sets the scroll height, that's the height of the content,
|
|
1173
|
-
*
|
|
1174
|
-
* @param {boolean=} forceUpdate If true, updates the height no matter what.
|
|
1175
|
-
*/
|
|
1176
|
-
_updateScrollerSize(forceUpdate) {
|
|
1177
|
-
const estScrollHeight =
|
|
1178
|
-
this._physicalBottom +
|
|
1179
|
-
Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage;
|
|
1180
|
-
|
|
1181
|
-
this._estScrollHeight = estScrollHeight;
|
|
1182
|
-
|
|
1183
|
-
// Amortize height adjustment, so it won't trigger large repaints too often.
|
|
1184
|
-
if (
|
|
1185
|
-
forceUpdate ||
|
|
1186
|
-
this._scrollHeight === 0 ||
|
|
1187
|
-
this._scrollPosition >= estScrollHeight - this._physicalSize ||
|
|
1188
|
-
Math.abs(estScrollHeight - this._scrollHeight) >= this._viewportHeight
|
|
1189
|
-
) {
|
|
1190
|
-
this.$.items.style.height = `${estScrollHeight}px`;
|
|
1191
|
-
this._scrollHeight = estScrollHeight;
|
|
1192
|
-
}
|
|
1193
|
-
},
|
|
1194
|
-
|
|
1195
|
-
/**
|
|
1196
|
-
* Scroll to a specific index in the virtual list regardless
|
|
1197
|
-
* of the physical items in the DOM tree.
|
|
1198
|
-
*
|
|
1199
|
-
* @method scrollToIndex
|
|
1200
|
-
* @param {number} idx The index of the item
|
|
1201
|
-
*/
|
|
1202
|
-
scrollToIndex(idx) {
|
|
1203
|
-
if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
flush();
|
|
1207
|
-
// Items should have been rendered prior scrolling to an index.
|
|
1208
|
-
if (this._physicalCount === 0) {
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
idx = this._clamp(idx, 0, this._virtualCount - 1);
|
|
1212
|
-
// Update the virtual start only when needed.
|
|
1213
|
-
if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
|
|
1214
|
-
this._virtualStart = idx - 1;
|
|
1215
|
-
}
|
|
1216
|
-
this._assignModels();
|
|
1217
|
-
this._updateMetrics();
|
|
1218
|
-
// Estimate new physical offset.
|
|
1219
|
-
this._physicalTop = this._virtualStart * this._physicalAverage;
|
|
1220
|
-
|
|
1221
|
-
let currentTopItem = this._physicalStart;
|
|
1222
|
-
let currentVirtualItem = this._virtualStart;
|
|
1223
|
-
let targetOffsetTop = 0;
|
|
1224
|
-
const hiddenContentSize = this._hiddenContentSize;
|
|
1225
|
-
// Scroll to the item as much as we can.
|
|
1226
|
-
while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
|
|
1227
|
-
targetOffsetTop += this._getPhysicalSizeIncrement(currentTopItem);
|
|
1228
|
-
currentTopItem = (currentTopItem + 1) % this._physicalCount;
|
|
1229
|
-
currentVirtualItem += 1;
|
|
1230
|
-
}
|
|
1231
|
-
this._updateScrollerSize(true);
|
|
1232
|
-
this._positionItems();
|
|
1233
|
-
this._resetScrollPosition(this._physicalTop + this._scrollOffset + targetOffsetTop);
|
|
1234
|
-
this._increasePoolIfNeeded(0);
|
|
1235
|
-
// Clear cached visible index.
|
|
1236
|
-
this._firstVisibleIndexVal = null;
|
|
1237
|
-
this._lastVisibleIndexVal = null;
|
|
1238
|
-
},
|
|
1239
|
-
|
|
1240
|
-
/**
|
|
1241
|
-
* Reset the physical average and the average count.
|
|
1242
|
-
*/
|
|
1243
|
-
_resetAverage() {
|
|
1244
|
-
this._physicalAverage = 0;
|
|
1245
|
-
this._physicalAverageCount = 0;
|
|
1246
|
-
},
|
|
1247
|
-
|
|
1248
|
-
/**
|
|
1249
|
-
* A handler for the `iron-resize` event triggered by `IronResizableBehavior`
|
|
1250
|
-
* when the element is resized.
|
|
1251
|
-
*/
|
|
1252
|
-
_resizeHandler() {
|
|
1253
|
-
this._debounce(
|
|
1254
|
-
'_render',
|
|
1255
|
-
() => {
|
|
1256
|
-
// Clear cached visible index.
|
|
1257
|
-
this._firstVisibleIndexVal = null;
|
|
1258
|
-
this._lastVisibleIndexVal = null;
|
|
1259
|
-
if (this._isVisible) {
|
|
1260
|
-
this.updateViewportBoundaries();
|
|
1261
|
-
// Reinstall the scroll event listener.
|
|
1262
|
-
this.toggleScrollListener(true);
|
|
1263
|
-
this._resetAverage();
|
|
1264
|
-
this._render();
|
|
1265
|
-
} else {
|
|
1266
|
-
// Uninstall the scroll event listener.
|
|
1267
|
-
this.toggleScrollListener(false);
|
|
1268
|
-
}
|
|
1269
|
-
},
|
|
1270
|
-
animationFrame,
|
|
1271
|
-
);
|
|
1272
|
-
},
|
|
1273
|
-
|
|
1274
|
-
_isIndexRendered(idx) {
|
|
1275
|
-
return idx >= this._virtualStart && idx <= this._virtualEnd;
|
|
1276
|
-
},
|
|
1277
|
-
|
|
1278
|
-
_getPhysicalIndex(vidx) {
|
|
1279
|
-
return (this._physicalStart + (vidx - this._virtualStart)) % this._physicalCount;
|
|
1280
|
-
},
|
|
1281
|
-
|
|
1282
|
-
_clamp(v, min, max) {
|
|
1283
|
-
return Math.min(max, Math.max(min, v));
|
|
1284
|
-
},
|
|
1285
|
-
|
|
1286
|
-
_debounce(name, cb, asyncModule) {
|
|
1287
|
-
if (!this._debouncers) {
|
|
1288
|
-
this._debouncers = {};
|
|
1289
|
-
}
|
|
1290
|
-
this._debouncers[name] = Debouncer.debounce(this._debouncers[name], asyncModule, cb.bind(this));
|
|
1291
|
-
enqueueDebouncer(this._debouncers[name]);
|
|
1292
|
-
},
|
|
1293
|
-
};
|
|
1294
|
-
|
|
1295
|
-
/**
|
|
1296
|
-
* @license
|
|
1297
|
-
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
1298
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
1299
|
-
*/
|
|
1300
|
-
|
|
1301
|
-
// Iron-list can by default handle sizes up to around 100000.
|
|
1302
|
-
// When the size is larger than MAX_VIRTUAL_COUNT _vidxOffset is used
|
|
1303
|
-
const MAX_VIRTUAL_COUNT = 100000;
|
|
1304
|
-
const OFFSET_ADJUST_MIN_THRESHOLD = 1000;
|
|
1305
|
-
|
|
1306
|
-
class IronListAdapter {
|
|
1307
|
-
constructor({ createElements, updateElement, scrollTarget, scrollContainer, elementsContainer, reorderElements }) {
|
|
1308
|
-
this.isAttached = true;
|
|
1309
|
-
this._vidxOffset = 0;
|
|
1310
|
-
this.createElements = createElements;
|
|
1311
|
-
this.updateElement = updateElement;
|
|
1312
|
-
this.scrollTarget = scrollTarget;
|
|
1313
|
-
this.scrollContainer = scrollContainer;
|
|
1314
|
-
this.elementsContainer = elementsContainer || scrollContainer;
|
|
1315
|
-
this.reorderElements = reorderElements;
|
|
1316
|
-
// Iron-list uses this value to determine how many pages of elements to render
|
|
1317
|
-
this._maxPages = 1.3;
|
|
1318
|
-
|
|
1319
|
-
// Placeholder height (used for sizing elements that have intrinsic 0 height after update)
|
|
1320
|
-
this.__placeholderHeight = 200;
|
|
1321
|
-
// A queue of 10 previous element heights
|
|
1322
|
-
this.__elementHeightQueue = Array(10);
|
|
1323
|
-
|
|
1324
|
-
this.timeouts = {
|
|
1325
|
-
SCROLL_REORDER: 500,
|
|
1326
|
-
IGNORE_WHEEL: 500,
|
|
1327
|
-
FIX_INVALID_ITEM_POSITIONING: 100,
|
|
1328
|
-
};
|
|
1329
|
-
|
|
1330
|
-
this.__resizeObserver = new ResizeObserver(() => this._resizeHandler());
|
|
1331
|
-
|
|
1332
|
-
if (getComputedStyle(this.scrollTarget).overflow === 'visible') {
|
|
1333
|
-
this.scrollTarget.style.overflow = 'auto';
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (getComputedStyle(this.scrollContainer).position === 'static') {
|
|
1337
|
-
this.scrollContainer.style.position = 'relative';
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
this.__resizeObserver.observe(this.scrollTarget);
|
|
1341
|
-
this.scrollTarget.addEventListener('scroll', () => this._scrollHandler());
|
|
1342
|
-
|
|
1343
|
-
this._scrollLineHeight = this._getScrollLineHeight();
|
|
1344
|
-
this.scrollTarget.addEventListener('wheel', (e) => this.__onWheel(e));
|
|
1345
|
-
|
|
1346
|
-
if (this.reorderElements) {
|
|
1347
|
-
// Reordering the physical elements cancels the user's grab of the scroll bar handle on Safari.
|
|
1348
|
-
// Need to defer reordering until the user lets go of the scroll bar handle.
|
|
1349
|
-
this.scrollTarget.addEventListener('mousedown', () => {
|
|
1350
|
-
this.__mouseDown = true;
|
|
1351
|
-
});
|
|
1352
|
-
this.scrollTarget.addEventListener('mouseup', () => {
|
|
1353
|
-
this.__mouseDown = false;
|
|
1354
|
-
if (this.__pendingReorder) {
|
|
1355
|
-
this.__reorderElements();
|
|
1356
|
-
}
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
get scrollOffset() {
|
|
1362
|
-
return 0;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
get adjustedFirstVisibleIndex() {
|
|
1366
|
-
return this.firstVisibleIndex + this._vidxOffset;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
get adjustedLastVisibleIndex() {
|
|
1370
|
-
return this.lastVisibleIndex + this._vidxOffset;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
scrollToIndex(index) {
|
|
1374
|
-
if (typeof index !== 'number' || isNaN(index) || this.size === 0 || !this.scrollTarget.offsetHeight) {
|
|
1375
|
-
return;
|
|
1376
|
-
}
|
|
1377
|
-
index = this._clamp(index, 0, this.size - 1);
|
|
1378
|
-
|
|
1379
|
-
const visibleElementCount = this.__getVisibleElements().length;
|
|
1380
|
-
let targetVirtualIndex = Math.floor((index / this.size) * this._virtualCount);
|
|
1381
|
-
if (this._virtualCount - targetVirtualIndex < visibleElementCount) {
|
|
1382
|
-
targetVirtualIndex = this._virtualCount - (this.size - index);
|
|
1383
|
-
this._vidxOffset = this.size - this._virtualCount;
|
|
1384
|
-
} else if (targetVirtualIndex < visibleElementCount) {
|
|
1385
|
-
if (index < OFFSET_ADJUST_MIN_THRESHOLD) {
|
|
1386
|
-
targetVirtualIndex = index;
|
|
1387
|
-
this._vidxOffset = 0;
|
|
1388
|
-
} else {
|
|
1389
|
-
targetVirtualIndex = OFFSET_ADJUST_MIN_THRESHOLD;
|
|
1390
|
-
this._vidxOffset = index - targetVirtualIndex;
|
|
1391
|
-
}
|
|
1392
|
-
} else {
|
|
1393
|
-
this._vidxOffset = index - targetVirtualIndex;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
this.__skipNextVirtualIndexAdjust = true;
|
|
1397
|
-
super.scrollToIndex(targetVirtualIndex);
|
|
1398
|
-
|
|
1399
|
-
if (this.adjustedFirstVisibleIndex !== index && this._scrollTop < this._maxScrollTop && !this.grid) {
|
|
1400
|
-
// Workaround an iron-list issue by manually adjusting the scroll position
|
|
1401
|
-
this._scrollTop -= this.__getIndexScrollOffset(index) || 0;
|
|
1402
|
-
}
|
|
1403
|
-
this._scrollHandler();
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
flush() {
|
|
1407
|
-
// The scroll target is hidden.
|
|
1408
|
-
if (this.scrollTarget.offsetHeight === 0) {
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
this._resizeHandler();
|
|
1413
|
-
flush();
|
|
1414
|
-
this._scrollHandler();
|
|
1415
|
-
if (this.__fixInvalidItemPositioningDebouncer) {
|
|
1416
|
-
this.__fixInvalidItemPositioningDebouncer.flush();
|
|
1417
|
-
}
|
|
1418
|
-
if (this.__scrollReorderDebouncer) {
|
|
1419
|
-
this.__scrollReorderDebouncer.flush();
|
|
1420
|
-
}
|
|
1421
|
-
if (this.__debouncerWheelAnimationFrame) {
|
|
1422
|
-
this.__debouncerWheelAnimationFrame.flush();
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
update(startIndex = 0, endIndex = this.size - 1) {
|
|
1427
|
-
const updatedElements = [];
|
|
1428
|
-
this.__getVisibleElements().forEach((el) => {
|
|
1429
|
-
if (el.__virtualIndex >= startIndex && el.__virtualIndex <= endIndex) {
|
|
1430
|
-
this.__updateElement(el, el.__virtualIndex, true);
|
|
1431
|
-
updatedElements.push(el);
|
|
1432
|
-
}
|
|
1433
|
-
});
|
|
1434
|
-
|
|
1435
|
-
this.__afterElementsUpdated(updatedElements);
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
/**
|
|
1439
|
-
* Updates the height for a given set of items.
|
|
1440
|
-
*
|
|
1441
|
-
* @param {!Array<number>=} itemSet
|
|
1442
|
-
*/
|
|
1443
|
-
_updateMetrics(itemSet) {
|
|
1444
|
-
// Make sure we distributed all the physical items
|
|
1445
|
-
// so we can measure them.
|
|
1446
|
-
flush();
|
|
1447
|
-
|
|
1448
|
-
let newPhysicalSize = 0;
|
|
1449
|
-
let oldPhysicalSize = 0;
|
|
1450
|
-
const prevAvgCount = this._physicalAverageCount;
|
|
1451
|
-
const prevPhysicalAvg = this._physicalAverage;
|
|
1452
|
-
|
|
1453
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1454
|
-
this._iterateItems((pidx, vidx) => {
|
|
1455
|
-
oldPhysicalSize += this._physicalSizes[pidx];
|
|
1456
|
-
this._physicalSizes[pidx] = Math.ceil(this.__getBorderBoxHeight(this._physicalItems[pidx]));
|
|
1457
|
-
newPhysicalSize += this._physicalSizes[pidx];
|
|
1458
|
-
this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
|
|
1459
|
-
}, itemSet);
|
|
1460
|
-
|
|
1461
|
-
this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
|
|
1462
|
-
|
|
1463
|
-
// Update the average if it measured something.
|
|
1464
|
-
if (this._physicalAverageCount !== prevAvgCount) {
|
|
1465
|
-
this._physicalAverage = Math.round(
|
|
1466
|
-
(prevPhysicalAvg * prevAvgCount + newPhysicalSize) / this._physicalAverageCount,
|
|
1467
|
-
);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
__getBorderBoxHeight(el) {
|
|
1472
|
-
const style = getComputedStyle(el);
|
|
1473
|
-
|
|
1474
|
-
const itemHeight = parseFloat(style.height) || 0;
|
|
1475
|
-
|
|
1476
|
-
if (style.boxSizing === 'border-box') {
|
|
1477
|
-
return itemHeight;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
1481
|
-
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
1482
|
-
const borderBottomWidth = parseFloat(style.borderBottomWidth) || 0;
|
|
1483
|
-
const borderTopWidth = parseFloat(style.borderTopWidth) || 0;
|
|
1484
|
-
|
|
1485
|
-
return itemHeight + paddingBottom + paddingTop + borderBottomWidth + borderTopWidth;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
__updateElement(el, index, forceSameIndexUpdates) {
|
|
1489
|
-
// Clean up temporary placeholder sizing
|
|
1490
|
-
if (el.style.paddingTop) {
|
|
1491
|
-
el.style.paddingTop = '';
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
if (!this.__preventElementUpdates && (el.__lastUpdatedIndex !== index || forceSameIndexUpdates)) {
|
|
1495
|
-
this.updateElement(el, index);
|
|
1496
|
-
el.__lastUpdatedIndex = index;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
/**
|
|
1501
|
-
* Called synchronously right after elements have been updated.
|
|
1502
|
-
* This is a good place to do any post-update work.
|
|
1503
|
-
*
|
|
1504
|
-
* @param {!Array<!HTMLElement>} updatedElements
|
|
1505
|
-
*/
|
|
1506
|
-
__afterElementsUpdated(updatedElements) {
|
|
1507
|
-
updatedElements.forEach((el) => {
|
|
1508
|
-
const elementHeight = el.offsetHeight;
|
|
1509
|
-
if (elementHeight === 0) {
|
|
1510
|
-
// If the elements have 0 height after update (for example due to lazy rendering),
|
|
1511
|
-
// it results in iron-list requesting to create an unlimited count of elements.
|
|
1512
|
-
// Assign a temporary placeholder sizing to elements that would otherwise end up having
|
|
1513
|
-
// no height.
|
|
1514
|
-
el.style.paddingTop = `${this.__placeholderHeight}px`;
|
|
1515
|
-
|
|
1516
|
-
// Manually schedule the resize handler to make sure the placeholder padding is
|
|
1517
|
-
// cleared in case the resize observer never triggers.
|
|
1518
|
-
this.__placeholderClearDebouncer = Debouncer.debounce(this.__placeholderClearDebouncer, animationFrame, () =>
|
|
1519
|
-
this._resizeHandler(),
|
|
1520
|
-
);
|
|
1521
|
-
} else {
|
|
1522
|
-
// Add element height to the queue
|
|
1523
|
-
this.__elementHeightQueue.push(elementHeight);
|
|
1524
|
-
this.__elementHeightQueue.shift();
|
|
1525
|
-
|
|
1526
|
-
// Calculate new placeholder height based on the average of the defined values in the
|
|
1527
|
-
// element height queue
|
|
1528
|
-
const filteredHeights = this.__elementHeightQueue.filter((h) => h !== undefined);
|
|
1529
|
-
this.__placeholderHeight = Math.round(filteredHeights.reduce((a, b) => a + b, 0) / filteredHeights.length);
|
|
1530
|
-
}
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
__getIndexScrollOffset(index) {
|
|
1535
|
-
const element = this.__getVisibleElements().find((el) => el.__virtualIndex === index);
|
|
1536
|
-
return element ? this.scrollTarget.getBoundingClientRect().top - element.getBoundingClientRect().top : undefined;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
get size() {
|
|
1540
|
-
return this.__size;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
set size(size) {
|
|
1544
|
-
if (size === this.size) {
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
// Cancel active debouncers
|
|
1548
|
-
if (this.__fixInvalidItemPositioningDebouncer) {
|
|
1549
|
-
this.__fixInvalidItemPositioningDebouncer.cancel();
|
|
1550
|
-
}
|
|
1551
|
-
if (this._debouncers && this._debouncers._increasePoolIfNeeded) {
|
|
1552
|
-
// Avoid creating unnecessary elements on the following flush()
|
|
1553
|
-
this._debouncers._increasePoolIfNeeded.cancel();
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// Change the size
|
|
1557
|
-
this.__size = size;
|
|
1558
|
-
|
|
1559
|
-
if (!this._physicalItems) {
|
|
1560
|
-
// Not initialized yet
|
|
1561
|
-
this._itemsChanged({
|
|
1562
|
-
path: 'items',
|
|
1563
|
-
});
|
|
1564
|
-
this.__preventElementUpdates = true;
|
|
1565
|
-
flush();
|
|
1566
|
-
this.__preventElementUpdates = false;
|
|
1567
|
-
} else {
|
|
1568
|
-
// Already initialized, just update _virtualCount
|
|
1569
|
-
this._updateScrollerSize();
|
|
1570
|
-
this._virtualCount = this.items.length;
|
|
1571
|
-
this._render();
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
// When reducing size while invisible, iron-list does not update items, so
|
|
1575
|
-
// their hidden state is not updated and their __lastUpdatedIndex is not
|
|
1576
|
-
// reset. In that case force an update here.
|
|
1577
|
-
if (!this._isVisible) {
|
|
1578
|
-
this._assignModels();
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
if (!this.elementsContainer.children.length) {
|
|
1582
|
-
requestAnimationFrame(() => this._resizeHandler());
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
// Schedule and flush a resize handler. This will cause a
|
|
1586
|
-
// re-render for the elements.
|
|
1587
|
-
this._resizeHandler();
|
|
1588
|
-
flush();
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
/** @private */
|
|
1592
|
-
get _scrollTop() {
|
|
1593
|
-
return this.scrollTarget.scrollTop;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
/** @private */
|
|
1597
|
-
set _scrollTop(top) {
|
|
1598
|
-
this.scrollTarget.scrollTop = top;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
/** @private */
|
|
1602
|
-
get items() {
|
|
1603
|
-
return {
|
|
1604
|
-
length: Math.min(this.size, MAX_VIRTUAL_COUNT),
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
/** @private */
|
|
1609
|
-
get offsetHeight() {
|
|
1610
|
-
return this.scrollTarget.offsetHeight;
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
/** @private */
|
|
1614
|
-
get $() {
|
|
1615
|
-
return {
|
|
1616
|
-
items: this.scrollContainer,
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
/** @private */
|
|
1621
|
-
updateViewportBoundaries() {
|
|
1622
|
-
const styles = window.getComputedStyle(this.scrollTarget);
|
|
1623
|
-
this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(styles['padding-top'], 10);
|
|
1624
|
-
this._isRTL = Boolean(styles.direction === 'rtl');
|
|
1625
|
-
this._viewportWidth = this.elementsContainer.offsetWidth;
|
|
1626
|
-
this._viewportHeight = this.scrollTarget.offsetHeight;
|
|
1627
|
-
this._scrollPageHeight = this._viewportHeight - this._scrollLineHeight;
|
|
1628
|
-
if (this.grid) {
|
|
1629
|
-
this._updateGridMetrics();
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
/** @private */
|
|
1634
|
-
setAttribute() {}
|
|
1635
|
-
|
|
1636
|
-
/** @private */
|
|
1637
|
-
_createPool(size) {
|
|
1638
|
-
const physicalItems = this.createElements(size);
|
|
1639
|
-
const fragment = document.createDocumentFragment();
|
|
1640
|
-
physicalItems.forEach((el) => {
|
|
1641
|
-
el.style.position = 'absolute';
|
|
1642
|
-
fragment.appendChild(el);
|
|
1643
|
-
this.__resizeObserver.observe(el);
|
|
1644
|
-
});
|
|
1645
|
-
this.elementsContainer.appendChild(fragment);
|
|
1646
|
-
return physicalItems;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
/** @private */
|
|
1650
|
-
_assignModels(itemSet) {
|
|
1651
|
-
const updatedElements = [];
|
|
1652
|
-
this._iterateItems((pidx, vidx) => {
|
|
1653
|
-
const el = this._physicalItems[pidx];
|
|
1654
|
-
el.hidden = vidx >= this.size;
|
|
1655
|
-
if (!el.hidden) {
|
|
1656
|
-
el.__virtualIndex = vidx + (this._vidxOffset || 0);
|
|
1657
|
-
this.__updateElement(el, el.__virtualIndex);
|
|
1658
|
-
updatedElements.push(el);
|
|
1659
|
-
} else {
|
|
1660
|
-
delete el.__lastUpdatedIndex;
|
|
1661
|
-
}
|
|
1662
|
-
}, itemSet);
|
|
1663
|
-
|
|
1664
|
-
this.__afterElementsUpdated(updatedElements);
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
/** @private */
|
|
1668
|
-
_isClientFull() {
|
|
1669
|
-
// Workaround an issue in iron-list that can cause it to freeze on fast scroll
|
|
1670
|
-
setTimeout(() => {
|
|
1671
|
-
this.__clientFull = true;
|
|
1672
|
-
});
|
|
1673
|
-
return this.__clientFull || super._isClientFull();
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
/** @private */
|
|
1677
|
-
translate3d(_x, y, _z, el) {
|
|
1678
|
-
el.style.transform = `translateY(${y})`;
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
/** @private */
|
|
1682
|
-
toggleScrollListener() {}
|
|
1683
|
-
|
|
1684
|
-
_scrollHandler() {
|
|
1685
|
-
// The scroll target is hidden.
|
|
1686
|
-
if (this.scrollTarget.offsetHeight === 0) {
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
this._adjustVirtualIndexOffset(this._scrollTop - (this.__previousScrollTop || 0));
|
|
1691
|
-
const delta = this.scrollTarget.scrollTop - this._scrollPosition;
|
|
1692
|
-
|
|
1693
|
-
super._scrollHandler();
|
|
1694
|
-
|
|
1695
|
-
if (this._physicalCount !== 0) {
|
|
1696
|
-
const isScrollingDown = delta >= 0;
|
|
1697
|
-
const reusables = this._getReusables(!isScrollingDown);
|
|
1698
|
-
|
|
1699
|
-
if (reusables.indexes.length) {
|
|
1700
|
-
// After running super._scrollHandler, fix internal properties to workaround an iron-list issue.
|
|
1701
|
-
// See https://github.com/vaadin/web-components/issues/1691
|
|
1702
|
-
this._physicalTop = reusables.physicalTop;
|
|
1703
|
-
|
|
1704
|
-
if (isScrollingDown) {
|
|
1705
|
-
this._virtualStart -= reusables.indexes.length;
|
|
1706
|
-
this._physicalStart -= reusables.indexes.length;
|
|
1707
|
-
} else {
|
|
1708
|
-
this._virtualStart += reusables.indexes.length;
|
|
1709
|
-
this._physicalStart += reusables.indexes.length;
|
|
1710
|
-
}
|
|
1711
|
-
this._resizeHandler();
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
if (delta) {
|
|
1716
|
-
// There was a change in scroll top. Schedule a check for invalid item positioning.
|
|
1717
|
-
this.__fixInvalidItemPositioningDebouncer = Debouncer.debounce(
|
|
1718
|
-
this.__fixInvalidItemPositioningDebouncer,
|
|
1719
|
-
timeOut.after(this.timeouts.FIX_INVALID_ITEM_POSITIONING),
|
|
1720
|
-
() => this.__fixInvalidItemPositioning(),
|
|
1721
|
-
);
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
if (this.reorderElements) {
|
|
1725
|
-
this.__scrollReorderDebouncer = Debouncer.debounce(
|
|
1726
|
-
this.__scrollReorderDebouncer,
|
|
1727
|
-
timeOut.after(this.timeouts.SCROLL_REORDER),
|
|
1728
|
-
() => this.__reorderElements(),
|
|
1729
|
-
);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
this.__previousScrollTop = this._scrollTop;
|
|
1733
|
-
|
|
1734
|
-
// If the first visible index is not 0 when scrolled to the top,
|
|
1735
|
-
// scroll to index 0 to fix the issue.
|
|
1736
|
-
if (this._scrollTop === 0 && this.firstVisibleIndex !== 0 && Math.abs(delta) > 0) {
|
|
1737
|
-
this.scrollToIndex(0);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
/**
|
|
1742
|
-
* Work around an iron-list issue with invalid item positioning.
|
|
1743
|
-
* See https://github.com/vaadin/flow-components/issues/4306
|
|
1744
|
-
* @private
|
|
1745
|
-
*/
|
|
1746
|
-
__fixInvalidItemPositioning() {
|
|
1747
|
-
if (!this.scrollTarget.isConnected) {
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
// Check if the first physical item element is below the top of the viewport
|
|
1752
|
-
const physicalTopBelowTop = this._physicalTop > this._scrollTop;
|
|
1753
|
-
// Check if the last physical item element is above the bottom of the viewport
|
|
1754
|
-
const physicalBottomAboveBottom = this._physicalBottom < this._scrollBottom;
|
|
1755
|
-
|
|
1756
|
-
// Check if the first index is visible
|
|
1757
|
-
const firstIndexVisible = this.adjustedFirstVisibleIndex === 0;
|
|
1758
|
-
// Check if the last index is visible
|
|
1759
|
-
const lastIndexVisible = this.adjustedLastVisibleIndex === this.size - 1;
|
|
1760
|
-
|
|
1761
|
-
if ((physicalTopBelowTop && !firstIndexVisible) || (physicalBottomAboveBottom && !lastIndexVisible)) {
|
|
1762
|
-
// Invalid state! Try to recover.
|
|
1763
|
-
|
|
1764
|
-
const isScrollingDown = physicalBottomAboveBottom;
|
|
1765
|
-
// Set the "_ratio" property temporarily to 0 to make iron-list's _getReusables
|
|
1766
|
-
// place all the free physical items on one side of the viewport.
|
|
1767
|
-
const originalRatio = this._ratio;
|
|
1768
|
-
this._ratio = 0;
|
|
1769
|
-
// Fake a scroll change to make _scrollHandler place the physical items
|
|
1770
|
-
// on the desired side.
|
|
1771
|
-
this._scrollPosition = this._scrollTop + (isScrollingDown ? -1 : 1);
|
|
1772
|
-
this._scrollHandler();
|
|
1773
|
-
// Restore the original "_ratio" value.
|
|
1774
|
-
this._ratio = originalRatio;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
/** @private */
|
|
1779
|
-
__onWheel(e) {
|
|
1780
|
-
if (e.ctrlKey || this._hasScrolledAncestor(e.target, e.deltaX, e.deltaY)) {
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
let deltaY = e.deltaY;
|
|
1785
|
-
if (e.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
|
1786
|
-
// Scrolling by "lines of text" instead of pixels
|
|
1787
|
-
deltaY *= this._scrollLineHeight;
|
|
1788
|
-
} else if (e.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
|
1789
|
-
// Scrolling by "pages" instead of pixels
|
|
1790
|
-
deltaY *= this._scrollPageHeight;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
if (!this._deltaYAcc) {
|
|
1794
|
-
this._deltaYAcc = 0;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
if (this._wheelAnimationFrame) {
|
|
1798
|
-
// Accumulate wheel delta while a frame is being processed
|
|
1799
|
-
this._deltaYAcc += deltaY;
|
|
1800
|
-
e.preventDefault();
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
deltaY += this._deltaYAcc;
|
|
1805
|
-
this._deltaYAcc = 0;
|
|
1806
|
-
|
|
1807
|
-
this._wheelAnimationFrame = true;
|
|
1808
|
-
this.__debouncerWheelAnimationFrame = Debouncer.debounce(
|
|
1809
|
-
this.__debouncerWheelAnimationFrame,
|
|
1810
|
-
animationFrame,
|
|
1811
|
-
() => {
|
|
1812
|
-
this._wheelAnimationFrame = false;
|
|
1813
|
-
},
|
|
1814
|
-
);
|
|
1815
|
-
|
|
1816
|
-
const momentum = Math.abs(e.deltaX) + Math.abs(deltaY);
|
|
1817
|
-
|
|
1818
|
-
if (this._canScroll(this.scrollTarget, e.deltaX, deltaY)) {
|
|
1819
|
-
e.preventDefault();
|
|
1820
|
-
this.scrollTarget.scrollTop += deltaY;
|
|
1821
|
-
this.scrollTarget.scrollLeft += e.deltaX;
|
|
1822
|
-
|
|
1823
|
-
this._hasResidualMomentum = true;
|
|
1824
|
-
|
|
1825
|
-
this._ignoreNewWheel = true;
|
|
1826
|
-
this._debouncerIgnoreNewWheel = Debouncer.debounce(
|
|
1827
|
-
this._debouncerIgnoreNewWheel,
|
|
1828
|
-
timeOut.after(this.timeouts.IGNORE_WHEEL),
|
|
1829
|
-
() => {
|
|
1830
|
-
this._ignoreNewWheel = false;
|
|
1831
|
-
},
|
|
1832
|
-
);
|
|
1833
|
-
} else if ((this._hasResidualMomentum && momentum <= this._previousMomentum) || this._ignoreNewWheel) {
|
|
1834
|
-
e.preventDefault();
|
|
1835
|
-
} else if (momentum > this._previousMomentum) {
|
|
1836
|
-
this._hasResidualMomentum = false;
|
|
1837
|
-
}
|
|
1838
|
-
this._previousMomentum = momentum;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
/**
|
|
1842
|
-
* Determines if the element has an ancestor that handles the scroll delta prior to this
|
|
1843
|
-
*
|
|
1844
|
-
* @private
|
|
1845
|
-
*/
|
|
1846
|
-
_hasScrolledAncestor(el, deltaX, deltaY) {
|
|
1847
|
-
if (el === this.scrollTarget || el === this.scrollTarget.getRootNode().host) {
|
|
1848
|
-
return false;
|
|
1849
|
-
} else if (
|
|
1850
|
-
this._canScroll(el, deltaX, deltaY) &&
|
|
1851
|
-
['auto', 'scroll'].indexOf(getComputedStyle(el).overflow) !== -1
|
|
1852
|
-
) {
|
|
1853
|
-
return true;
|
|
1854
|
-
} else if (el !== this && el.parentElement) {
|
|
1855
|
-
return this._hasScrolledAncestor(el.parentElement, deltaX, deltaY);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
_canScroll(el, deltaX, deltaY) {
|
|
1860
|
-
return (
|
|
1861
|
-
(deltaY > 0 && el.scrollTop < el.scrollHeight - el.offsetHeight) ||
|
|
1862
|
-
(deltaY < 0 && el.scrollTop > 0) ||
|
|
1863
|
-
(deltaX > 0 && el.scrollLeft < el.scrollWidth - el.offsetWidth) ||
|
|
1864
|
-
(deltaX < 0 && el.scrollLeft > 0)
|
|
1865
|
-
);
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
/**
|
|
1869
|
-
* Increases the pool size.
|
|
1870
|
-
* @override
|
|
1871
|
-
*/
|
|
1872
|
-
_increasePoolIfNeeded(count) {
|
|
1873
|
-
if (this._physicalCount > 2 && count) {
|
|
1874
|
-
// The iron-list logic has already created some physical items and
|
|
1875
|
-
// has decided to create more. Since each item creation round is
|
|
1876
|
-
// expensive, let's try to create the remaining items in one go.
|
|
1877
|
-
|
|
1878
|
-
// Calculate the total item count that would be needed to fill the viewport
|
|
1879
|
-
// plus the buffer assuming rest of the items to be of the average size
|
|
1880
|
-
// of the items already created.
|
|
1881
|
-
const totalItemCount = Math.ceil(this._optPhysicalSize / this._physicalAverage);
|
|
1882
|
-
const missingItemCount = totalItemCount - this._physicalCount;
|
|
1883
|
-
// Create the remaining items in one go. Use a maximum of 100 items
|
|
1884
|
-
// as a safety measure.
|
|
1885
|
-
super._increasePoolIfNeeded(Math.max(count, Math.min(100, missingItemCount)));
|
|
1886
|
-
} else {
|
|
1887
|
-
super._increasePoolIfNeeded(count);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
/**
|
|
1892
|
-
* @returns {Number|undefined} - The browser's default font-size in pixels
|
|
1893
|
-
* @private
|
|
1894
|
-
*/
|
|
1895
|
-
_getScrollLineHeight() {
|
|
1896
|
-
const el = document.createElement('div');
|
|
1897
|
-
el.style.fontSize = 'initial';
|
|
1898
|
-
el.style.display = 'none';
|
|
1899
|
-
document.body.appendChild(el);
|
|
1900
|
-
const fontSize = window.getComputedStyle(el).fontSize;
|
|
1901
|
-
document.body.removeChild(el);
|
|
1902
|
-
return fontSize ? window.parseInt(fontSize) : undefined;
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
__getVisibleElements() {
|
|
1906
|
-
return Array.from(this.elementsContainer.children).filter((element) => !element.hidden);
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
/** @private */
|
|
1910
|
-
__reorderElements() {
|
|
1911
|
-
if (this.__mouseDown) {
|
|
1912
|
-
this.__pendingReorder = true;
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
|
-
this.__pendingReorder = false;
|
|
1916
|
-
|
|
1917
|
-
const adjustedVirtualStart = this._virtualStart + (this._vidxOffset || 0);
|
|
1918
|
-
|
|
1919
|
-
// Which row to use as a target?
|
|
1920
|
-
const visibleElements = this.__getVisibleElements();
|
|
1921
|
-
|
|
1922
|
-
const elementWithFocus = visibleElements.find(
|
|
1923
|
-
(element) =>
|
|
1924
|
-
element.contains(this.elementsContainer.getRootNode().activeElement) ||
|
|
1925
|
-
element.contains(this.scrollTarget.getRootNode().activeElement),
|
|
1926
|
-
);
|
|
1927
|
-
const targetElement = elementWithFocus || visibleElements[0];
|
|
1928
|
-
if (!targetElement) {
|
|
1929
|
-
// All elements are hidden, don't reorder
|
|
1930
|
-
return;
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
// Where the target row should be?
|
|
1934
|
-
const targetPhysicalIndex = targetElement.__virtualIndex - adjustedVirtualStart;
|
|
1935
|
-
|
|
1936
|
-
// Reodrer the DOM elements to keep the target row at the target physical index
|
|
1937
|
-
const delta = visibleElements.indexOf(targetElement) - targetPhysicalIndex;
|
|
1938
|
-
if (delta > 0) {
|
|
1939
|
-
for (let i = 0; i < delta; i++) {
|
|
1940
|
-
this.elementsContainer.appendChild(visibleElements[i]);
|
|
1941
|
-
}
|
|
1942
|
-
} else if (delta < 0) {
|
|
1943
|
-
for (let i = visibleElements.length + delta; i < visibleElements.length; i++) {
|
|
1944
|
-
this.elementsContainer.insertBefore(visibleElements[i], visibleElements[0]);
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
// Due to a rendering bug, reordering the rows can make parts of the scroll target disappear
|
|
1949
|
-
// on Safari when using sticky positioning in case the scroll target is inside a flexbox.
|
|
1950
|
-
// This issue manifests with grid (the header can disappear if grid is used inside a flexbox)
|
|
1951
|
-
if (isSafari) {
|
|
1952
|
-
const { transform } = this.scrollTarget.style;
|
|
1953
|
-
this.scrollTarget.style.transform = 'translateZ(0)';
|
|
1954
|
-
setTimeout(() => {
|
|
1955
|
-
this.scrollTarget.style.transform = transform;
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
/** @private */
|
|
1961
|
-
_adjustVirtualIndexOffset(delta) {
|
|
1962
|
-
if (this._virtualCount >= this.size) {
|
|
1963
|
-
this._vidxOffset = 0;
|
|
1964
|
-
} else if (this.__skipNextVirtualIndexAdjust) {
|
|
1965
|
-
this.__skipNextVirtualIndexAdjust = false;
|
|
1966
|
-
} else if (Math.abs(delta) > 10000) {
|
|
1967
|
-
// Process a large scroll position change
|
|
1968
|
-
const scale = this._scrollTop / (this.scrollTarget.scrollHeight - this.scrollTarget.offsetHeight);
|
|
1969
|
-
const offset = scale * this.size;
|
|
1970
|
-
this._vidxOffset = Math.round(offset - scale * this._virtualCount);
|
|
1971
|
-
} else {
|
|
1972
|
-
// Make sure user can always swipe/wheel scroll to the start and end
|
|
1973
|
-
const oldOffset = this._vidxOffset;
|
|
1974
|
-
const threshold = OFFSET_ADJUST_MIN_THRESHOLD;
|
|
1975
|
-
const maxShift = 100;
|
|
1976
|
-
|
|
1977
|
-
// Near start
|
|
1978
|
-
if (this._scrollTop === 0) {
|
|
1979
|
-
this._vidxOffset = 0;
|
|
1980
|
-
if (oldOffset !== this._vidxOffset) {
|
|
1981
|
-
super.scrollToIndex(0);
|
|
1982
|
-
}
|
|
1983
|
-
} else if (this.firstVisibleIndex < threshold && this._vidxOffset > 0) {
|
|
1984
|
-
this._vidxOffset -= Math.min(this._vidxOffset, maxShift);
|
|
1985
|
-
super.scrollToIndex(this.firstVisibleIndex + (oldOffset - this._vidxOffset));
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
// Near end
|
|
1989
|
-
const maxOffset = this.size - this._virtualCount;
|
|
1990
|
-
if (this._scrollTop >= this._maxScrollTop && this._maxScrollTop > 0) {
|
|
1991
|
-
this._vidxOffset = maxOffset;
|
|
1992
|
-
if (oldOffset !== this._vidxOffset) {
|
|
1993
|
-
super.scrollToIndex(this._virtualCount - 1);
|
|
1994
|
-
}
|
|
1995
|
-
} else if (this.firstVisibleIndex > this._virtualCount - threshold && this._vidxOffset < maxOffset) {
|
|
1996
|
-
this._vidxOffset += Math.min(maxOffset - this._vidxOffset, maxShift);
|
|
1997
|
-
super.scrollToIndex(this.firstVisibleIndex - (this._vidxOffset - oldOffset));
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
Object.setPrototypeOf(IronListAdapter.prototype, ironList);
|
|
2004
|
-
|
|
2005
|
-
class Virtualizer {
|
|
2006
|
-
/**
|
|
2007
|
-
* @typedef {Object} VirtualizerConfig
|
|
2008
|
-
* @property {Function} createElements Function that returns the given number of new elements
|
|
2009
|
-
* @property {Function} updateElement Function that updates the element at a specific index
|
|
2010
|
-
* @property {HTMLElement} scrollTarget Reference to the scrolling element
|
|
2011
|
-
* @property {HTMLElement} scrollContainer Reference to a wrapper for the item elements (or a slot) inside the scrollTarget
|
|
2012
|
-
* @property {HTMLElement | undefined} elementsContainer Reference to the container in which the item elements are placed, defaults to scrollContainer
|
|
2013
|
-
* @property {boolean | undefined} reorderElements Determines whether the physical item elements should be kept in order in the DOM
|
|
2014
|
-
* @param {VirtualizerConfig} config Configuration for the virtualizer
|
|
2015
|
-
*/
|
|
2016
|
-
constructor(config) {
|
|
2017
|
-
this.__adapter = new IronListAdapter(config);
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
/**
|
|
2021
|
-
* Gets the index of the first visible item in the viewport.
|
|
2022
|
-
*
|
|
2023
|
-
* @return {number}
|
|
2024
|
-
*/
|
|
2025
|
-
get firstVisibleIndex() {
|
|
2026
|
-
return this.__adapter.adjustedFirstVisibleIndex;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
/**
|
|
2030
|
-
* Gets the index of the last visible item in the viewport.
|
|
2031
|
-
*
|
|
2032
|
-
* @return {number}
|
|
2033
|
-
*/
|
|
2034
|
-
get lastVisibleIndex() {
|
|
2035
|
-
return this.__adapter.adjustedLastVisibleIndex;
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
/**
|
|
2039
|
-
* The size of the virtualizer
|
|
2040
|
-
* @return {number | undefined} The size of the virtualizer
|
|
2041
|
-
*/
|
|
2042
|
-
get size() {
|
|
2043
|
-
return this.__adapter.size;
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
/**
|
|
2047
|
-
* The size of the virtualizer
|
|
2048
|
-
* @param {number} size The size of the virtualizer
|
|
2049
|
-
*/
|
|
2050
|
-
set size(size) {
|
|
2051
|
-
this.__adapter.size = size;
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
/**
|
|
2055
|
-
* Scroll to a specific index in the virtual list
|
|
2056
|
-
*
|
|
2057
|
-
* @method scrollToIndex
|
|
2058
|
-
* @param {number} index The index of the item
|
|
2059
|
-
*/
|
|
2060
|
-
scrollToIndex(index) {
|
|
2061
|
-
this.__adapter.scrollToIndex(index);
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
/**
|
|
2065
|
-
* Requests the virtualizer to re-render the item elements on an index range, if currently in the DOM
|
|
2066
|
-
*
|
|
2067
|
-
* @method update
|
|
2068
|
-
* @param {number | undefined} startIndex The start index of the range
|
|
2069
|
-
* @param {number | undefined} endIndex The end index of the range
|
|
2070
|
-
*/
|
|
2071
|
-
update(startIndex = 0, endIndex = this.size - 1) {
|
|
2072
|
-
this.__adapter.update(startIndex, endIndex);
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
/**
|
|
2076
|
-
* Flushes active asynchronous tasks so that the component and the DOM end up in a stable state
|
|
2077
|
-
*
|
|
2078
|
-
* @method update
|
|
2079
|
-
* @param {number | undefined} startIndex The start index of the range
|
|
2080
|
-
* @param {number | undefined} endIndex The end index of the range
|
|
2081
|
-
*/
|
|
2082
|
-
flush() {
|
|
2083
|
-
this.__adapter.flush();
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
/**
|
|
2088
|
-
* @license
|
|
2089
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
2090
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
2091
|
-
*/
|
|
2092
|
-
|
|
2093
|
-
/*
|
|
2094
|
-
* Placeholder object class representing items being loaded.
|
|
2095
|
-
*
|
|
2096
|
-
* @private
|
|
2097
|
-
*/
|
|
2098
|
-
const ComboBoxPlaceholder = class ComboBoxPlaceholder {
|
|
2099
|
-
toString() {
|
|
2100
|
-
return '';
|
|
2101
|
-
}
|
|
2102
|
-
};
|
|
2103
|
-
|
|
2104
|
-
/**
|
|
2105
|
-
* @license
|
|
2106
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
2107
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
2108
|
-
*/
|
|
2109
|
-
|
|
2110
|
-
/**
|
|
2111
|
-
* @polymerMixin
|
|
2112
|
-
*/
|
|
2113
|
-
const ComboBoxScrollerMixin = (superClass) =>
|
|
2114
|
-
class ComboBoxScrollerMixin extends superClass {
|
|
2115
|
-
static get properties() {
|
|
2116
|
-
return {
|
|
2117
|
-
/**
|
|
2118
|
-
* A full set of items to filter the visible options from.
|
|
2119
|
-
* Set to an empty array when combo-box is not opened.
|
|
2120
|
-
*/
|
|
2121
|
-
items: {
|
|
2122
|
-
type: Array,
|
|
2123
|
-
observer: '__itemsChanged',
|
|
2124
|
-
},
|
|
2125
|
-
|
|
2126
|
-
/**
|
|
2127
|
-
* Index of an item that has focus outline and is scrolled into view.
|
|
2128
|
-
* The actual focus still remains in the input field.
|
|
2129
|
-
*/
|
|
2130
|
-
focusedIndex: {
|
|
2131
|
-
type: Number,
|
|
2132
|
-
observer: '__focusedIndexChanged',
|
|
2133
|
-
},
|
|
2134
|
-
|
|
2135
|
-
/**
|
|
2136
|
-
* Set to true while combo-box fetches new page from the data provider.
|
|
2137
|
-
*/
|
|
2138
|
-
loading: {
|
|
2139
|
-
type: Boolean,
|
|
2140
|
-
observer: '__loadingChanged',
|
|
2141
|
-
},
|
|
2142
|
-
|
|
2143
|
-
/**
|
|
2144
|
-
* Whether the combo-box is currently opened or not. If set to false,
|
|
2145
|
-
* calling `scrollIntoView` does not have any effect.
|
|
2146
|
-
*/
|
|
2147
|
-
opened: {
|
|
2148
|
-
type: Boolean,
|
|
2149
|
-
observer: '__openedChanged',
|
|
2150
|
-
},
|
|
2151
|
-
|
|
2152
|
-
/**
|
|
2153
|
-
* The selected item from the `items` array.
|
|
2154
|
-
*/
|
|
2155
|
-
selectedItem: {
|
|
2156
|
-
type: Object,
|
|
2157
|
-
observer: '__selectedItemChanged',
|
|
2158
|
-
},
|
|
2159
|
-
|
|
2160
|
-
/**
|
|
2161
|
-
* Path for the id of the item, used to detect whether the item is selected.
|
|
2162
|
-
*/
|
|
2163
|
-
itemIdPath: {
|
|
2164
|
-
type: String,
|
|
2165
|
-
},
|
|
2166
|
-
|
|
2167
|
-
/**
|
|
2168
|
-
* Reference to the owner (combo-box owner), used by the item elements.
|
|
2169
|
-
*/
|
|
2170
|
-
owner: {
|
|
2171
|
-
type: Object,
|
|
2172
|
-
},
|
|
2173
|
-
|
|
2174
|
-
/**
|
|
2175
|
-
* Function used to set a label for every combo-box item.
|
|
2176
|
-
*/
|
|
2177
|
-
getItemLabel: {
|
|
2178
|
-
type: Object,
|
|
2179
|
-
},
|
|
2180
|
-
|
|
2181
|
-
/**
|
|
2182
|
-
* Function used to render the content of every combo-box item.
|
|
2183
|
-
*/
|
|
2184
|
-
renderer: {
|
|
2185
|
-
type: Object,
|
|
2186
|
-
observer: '__rendererChanged',
|
|
2187
|
-
},
|
|
2188
|
-
|
|
2189
|
-
/**
|
|
2190
|
-
* Used to propagate the `theme` attribute from the host element.
|
|
2191
|
-
*/
|
|
2192
|
-
theme: {
|
|
2193
|
-
type: String,
|
|
2194
|
-
},
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
constructor() {
|
|
2199
|
-
super();
|
|
2200
|
-
this.__boundOnItemClick = this.__onItemClick.bind(this);
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
/** @private */
|
|
2204
|
-
get _viewportTotalPaddingBottom() {
|
|
2205
|
-
if (this._cachedViewportTotalPaddingBottom === undefined) {
|
|
2206
|
-
const itemsStyle = window.getComputedStyle(this.$.selector);
|
|
2207
|
-
this._cachedViewportTotalPaddingBottom = [itemsStyle.paddingBottom, itemsStyle.borderBottomWidth]
|
|
2208
|
-
.map((v) => {
|
|
2209
|
-
return parseInt(v, 10);
|
|
2210
|
-
})
|
|
2211
|
-
.reduce((sum, v) => {
|
|
2212
|
-
return sum + v;
|
|
2213
|
-
});
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
return this._cachedViewportTotalPaddingBottom;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
/** @protected */
|
|
2220
|
-
ready() {
|
|
2221
|
-
super.ready();
|
|
2222
|
-
|
|
2223
|
-
this.setAttribute('role', 'listbox');
|
|
2224
|
-
|
|
2225
|
-
// Ensure every instance has unique ID
|
|
2226
|
-
this.id = `${this.localName}-${generateUniqueId()}`;
|
|
2227
|
-
|
|
2228
|
-
// Allow extensions to customize tag name for the items
|
|
2229
|
-
this.__hostTagName = this.constructor.is.replace('-scroller', '');
|
|
2230
|
-
|
|
2231
|
-
this.addEventListener('click', (e) => e.stopPropagation());
|
|
2232
|
-
|
|
2233
|
-
this.__patchWheelOverScrolling();
|
|
2234
|
-
|
|
2235
|
-
this.__virtualizer = new Virtualizer({
|
|
2236
|
-
createElements: this.__createElements.bind(this),
|
|
2237
|
-
updateElement: this._updateElement.bind(this),
|
|
2238
|
-
elementsContainer: this,
|
|
2239
|
-
scrollTarget: this,
|
|
2240
|
-
scrollContainer: this.$.selector,
|
|
2241
|
-
});
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
/**
|
|
2245
|
-
* Requests an update for the virtualizer to re-render items.
|
|
2246
|
-
*/
|
|
2247
|
-
requestContentUpdate() {
|
|
2248
|
-
if (this.__virtualizer) {
|
|
2249
|
-
this.__virtualizer.update();
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
/**
|
|
2254
|
-
* Scrolls an item at given index into view and adjusts `scrollTop`
|
|
2255
|
-
* so that the element gets fully visible on Arrow Down key press.
|
|
2256
|
-
* @param {number} index
|
|
2257
|
-
*/
|
|
2258
|
-
scrollIntoView(index) {
|
|
2259
|
-
if (!(this.opened && index >= 0)) {
|
|
2260
|
-
return;
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
const visibleItemsCount = this._visibleItemsCount();
|
|
2264
|
-
|
|
2265
|
-
let targetIndex = index;
|
|
2266
|
-
|
|
2267
|
-
if (index > this.__virtualizer.lastVisibleIndex - 1) {
|
|
2268
|
-
// Index is below the bottom, scrolling down. Make the item appear at the bottom.
|
|
2269
|
-
// First scroll to target (will be at the top of the scroller) to make sure it's rendered.
|
|
2270
|
-
this.__virtualizer.scrollToIndex(index);
|
|
2271
|
-
// Then calculate the index for the following scroll (to get the target to bottom of the scroller).
|
|
2272
|
-
targetIndex = index - visibleItemsCount + 1;
|
|
2273
|
-
} else if (index > this.__virtualizer.firstVisibleIndex) {
|
|
2274
|
-
// The item is already visible, scrolling is unnecessary per se. But we need to trigger iron-list to set
|
|
2275
|
-
// the correct scrollTop on the scrollTarget. Scrolling to firstVisibleIndex.
|
|
2276
|
-
targetIndex = this.__virtualizer.firstVisibleIndex;
|
|
2277
|
-
}
|
|
2278
|
-
this.__virtualizer.scrollToIndex(Math.max(0, targetIndex));
|
|
2279
|
-
|
|
2280
|
-
// Sometimes the item is partly below the bottom edge, detect and adjust.
|
|
2281
|
-
const lastPhysicalItem = [...this.children].find(
|
|
2282
|
-
(el) => !el.hidden && el.index === this.__virtualizer.lastVisibleIndex,
|
|
2283
|
-
);
|
|
2284
|
-
if (!lastPhysicalItem || index !== lastPhysicalItem.index) {
|
|
2285
|
-
return;
|
|
2286
|
-
}
|
|
2287
|
-
const lastPhysicalItemRect = lastPhysicalItem.getBoundingClientRect();
|
|
2288
|
-
const scrollerRect = this.getBoundingClientRect();
|
|
2289
|
-
const scrollTopAdjust = lastPhysicalItemRect.bottom - scrollerRect.bottom + this._viewportTotalPaddingBottom;
|
|
2290
|
-
if (scrollTopAdjust > 0) {
|
|
2291
|
-
this.scrollTop += scrollTopAdjust;
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
/**
|
|
2296
|
-
* @param {string | object} item
|
|
2297
|
-
* @param {string | object} selectedItem
|
|
2298
|
-
* @param {string} itemIdPath
|
|
2299
|
-
* @protected
|
|
2300
|
-
*/
|
|
2301
|
-
_isItemSelected(item, selectedItem, itemIdPath) {
|
|
2302
|
-
if (item instanceof ComboBoxPlaceholder) {
|
|
2303
|
-
return false;
|
|
2304
|
-
} else if (itemIdPath && item !== undefined && selectedItem !== undefined) {
|
|
2305
|
-
return get(itemIdPath, item) === get(itemIdPath, selectedItem);
|
|
2306
|
-
}
|
|
2307
|
-
return item === selectedItem;
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
/** @private */
|
|
2311
|
-
__itemsChanged(items) {
|
|
2312
|
-
if (this.__virtualizer && items) {
|
|
2313
|
-
this.__virtualizer.size = items.length;
|
|
2314
|
-
this.__virtualizer.flush();
|
|
2315
|
-
this.requestContentUpdate();
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
/** @private */
|
|
2320
|
-
__loadingChanged() {
|
|
2321
|
-
this.requestContentUpdate();
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
/** @private */
|
|
2325
|
-
__openedChanged(opened) {
|
|
2326
|
-
if (opened) {
|
|
2327
|
-
this.requestContentUpdate();
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
/** @private */
|
|
2332
|
-
__selectedItemChanged() {
|
|
2333
|
-
this.requestContentUpdate();
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
/** @private */
|
|
2337
|
-
__focusedIndexChanged(index, oldIndex) {
|
|
2338
|
-
if (index !== oldIndex) {
|
|
2339
|
-
this.requestContentUpdate();
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
// Do not jump back to the previously focused item while loading
|
|
2343
|
-
// when requesting next page from the data provider on scroll.
|
|
2344
|
-
if (index >= 0 && !this.loading) {
|
|
2345
|
-
this.scrollIntoView(index);
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
/** @private */
|
|
2350
|
-
__rendererChanged(renderer, oldRenderer) {
|
|
2351
|
-
if (renderer || oldRenderer) {
|
|
2352
|
-
this.requestContentUpdate();
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
/** @private */
|
|
2357
|
-
__createElements(count) {
|
|
2358
|
-
return [...Array(count)].map(() => {
|
|
2359
|
-
const item = document.createElement(`${this.__hostTagName}-item`);
|
|
2360
|
-
item.addEventListener('click', this.__boundOnItemClick);
|
|
2361
|
-
// Negative tabindex prevents the item content from being focused.
|
|
2362
|
-
item.tabIndex = '-1';
|
|
2363
|
-
item.style.width = '100%';
|
|
2364
|
-
return item;
|
|
2365
|
-
});
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
/**
|
|
2369
|
-
* @param {HTMLElement} el
|
|
2370
|
-
* @param {number} index
|
|
2371
|
-
* @protected
|
|
2372
|
-
*/
|
|
2373
|
-
_updateElement(el, index) {
|
|
2374
|
-
const item = this.items[index];
|
|
2375
|
-
const focusedIndex = this.focusedIndex;
|
|
2376
|
-
const selected = this._isItemSelected(item, this.selectedItem, this.itemIdPath);
|
|
2377
|
-
|
|
2378
|
-
el.setProperties({
|
|
2379
|
-
item,
|
|
2380
|
-
index,
|
|
2381
|
-
label: this.getItemLabel(item),
|
|
2382
|
-
selected,
|
|
2383
|
-
renderer: this.renderer,
|
|
2384
|
-
focused: !this.loading && focusedIndex === index,
|
|
2385
|
-
});
|
|
2386
|
-
|
|
2387
|
-
el.id = `${this.__hostTagName}-item-${index}`;
|
|
2388
|
-
el.setAttribute('role', index !== undefined ? 'option' : false);
|
|
2389
|
-
el.setAttribute('aria-selected', selected.toString());
|
|
2390
|
-
el.setAttribute('aria-posinset', index + 1);
|
|
2391
|
-
el.setAttribute('aria-setsize', this.items.length);
|
|
2392
|
-
|
|
2393
|
-
if (this.theme) {
|
|
2394
|
-
el.setAttribute('theme', this.theme);
|
|
2395
|
-
} else {
|
|
2396
|
-
el.removeAttribute('theme');
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
if (item instanceof ComboBoxPlaceholder) {
|
|
2400
|
-
this.__requestItemByIndex(index);
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
/** @private */
|
|
2405
|
-
__onItemClick(e) {
|
|
2406
|
-
this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.currentTarget.item } }));
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
/**
|
|
2410
|
-
* We want to prevent the kinetic scrolling energy from being transferred from the overlay contents over to the parent.
|
|
2411
|
-
* Further improvement ideas: after the contents have been scrolled to the top or bottom and scrolling has stopped, it could allow
|
|
2412
|
-
* scrolling the parent similarly to touch scrolling.
|
|
2413
|
-
* @private
|
|
2414
|
-
*/
|
|
2415
|
-
__patchWheelOverScrolling() {
|
|
2416
|
-
this.$.selector.addEventListener('wheel', (e) => {
|
|
2417
|
-
const scrolledToTop = this.scrollTop === 0;
|
|
2418
|
-
const scrolledToBottom = this.scrollHeight - this.scrollTop - this.clientHeight <= 1;
|
|
2419
|
-
if (scrolledToTop && e.deltaY < 0) {
|
|
2420
|
-
e.preventDefault();
|
|
2421
|
-
} else if (scrolledToBottom && e.deltaY > 0) {
|
|
2422
|
-
e.preventDefault();
|
|
2423
|
-
}
|
|
2424
|
-
});
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
|
-
/**
|
|
2428
|
-
* Dispatches an `index-requested` event for the given index to notify
|
|
2429
|
-
* the data provider that it should start loading the page containing the requested index.
|
|
2430
|
-
*
|
|
2431
|
-
* The event is dispatched asynchronously to prevent an immediate page request and therefore
|
|
2432
|
-
* a possible infinite recursion in case the data provider implements page request cancelation logic
|
|
2433
|
-
* by invoking data provider page callbacks with an empty array.
|
|
2434
|
-
* The infinite recursion may occur otherwise since invoking a data provider page callback with an empty array
|
|
2435
|
-
* triggers a synchronous scroller update and, if the callback corresponds to the currently visible page,
|
|
2436
|
-
* the scroller will synchronously request the page again which may lead to looping in the end.
|
|
2437
|
-
* That was the case for the Flow counterpart:
|
|
2438
|
-
* https://github.com/vaadin/flow-components/issues/3553#issuecomment-1239344828
|
|
2439
|
-
* @private
|
|
2440
|
-
*/
|
|
2441
|
-
__requestItemByIndex(index) {
|
|
2442
|
-
requestAnimationFrame(() => {
|
|
2443
|
-
this.dispatchEvent(
|
|
2444
|
-
new CustomEvent('index-requested', {
|
|
2445
|
-
detail: {
|
|
2446
|
-
index,
|
|
2447
|
-
currentScrollerPos: this._oldScrollerPosition,
|
|
2448
|
-
},
|
|
2449
|
-
}),
|
|
2450
|
-
);
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
/** @private */
|
|
2455
|
-
_visibleItemsCount() {
|
|
2456
|
-
// Ensure items are positioned
|
|
2457
|
-
this.__virtualizer.scrollToIndex(this.__virtualizer.firstVisibleIndex);
|
|
2458
|
-
const hasItems = this.__virtualizer.size > 0;
|
|
2459
|
-
return hasItems ? this.__virtualizer.lastVisibleIndex - this.__virtualizer.firstVisibleIndex + 1 : 0;
|
|
2460
|
-
}
|
|
2461
|
-
};
|
|
2462
|
-
|
|
2463
|
-
/**
|
|
2464
|
-
* @license
|
|
2465
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
2466
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
2467
|
-
*/
|
|
2468
|
-
|
|
2469
|
-
/**
|
|
2470
|
-
* An element used internally by `<vaadin-combo-box>`. Not intended to be used separately.
|
|
2471
|
-
*
|
|
2472
|
-
* @customElement
|
|
2473
|
-
* @extends HTMLElement
|
|
2474
|
-
* @mixes ComboBoxScrollerMixin
|
|
2475
|
-
* @private
|
|
2476
|
-
*/
|
|
2477
|
-
class ComboBoxScroller extends ComboBoxScrollerMixin(PolymerElement) {
|
|
2478
|
-
static get is() {
|
|
2479
|
-
return 'vaadin-combo-box-scroller';
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2482
|
-
static get template() {
|
|
2483
|
-
return html`
|
|
2484
|
-
<style>
|
|
2485
|
-
:host {
|
|
2486
|
-
display: block;
|
|
2487
|
-
min-height: 1px;
|
|
2488
|
-
overflow: auto;
|
|
2489
|
-
|
|
2490
|
-
/* Fixes item background from getting on top of scrollbars on Safari */
|
|
2491
|
-
transform: translate3d(0, 0, 0);
|
|
2492
|
-
|
|
2493
|
-
/* Enable momentum scrolling on iOS */
|
|
2494
|
-
-webkit-overflow-scrolling: touch;
|
|
2495
|
-
|
|
2496
|
-
/* Fixes scrollbar disappearing when 'Show scroll bars: Always' enabled in Safari */
|
|
2497
|
-
box-shadow: 0 0 0 white;
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
#selector {
|
|
2501
|
-
border-width: var(--_vaadin-combo-box-items-container-border-width);
|
|
2502
|
-
border-style: var(--_vaadin-combo-box-items-container-border-style);
|
|
2503
|
-
border-color: var(--_vaadin-combo-box-items-container-border-color, transparent);
|
|
2504
|
-
position: relative;
|
|
2505
|
-
}
|
|
2506
|
-
</style>
|
|
2507
|
-
<div id="selector">
|
|
2508
|
-
<slot></slot>
|
|
2509
|
-
</div>
|
|
2510
|
-
`;
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
defineCustomElement(ComboBoxScroller);
|
|
2515
|
-
|
|
2516
|
-
/**
|
|
2517
|
-
* @license
|
|
2518
|
-
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
2519
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
2520
|
-
*/
|
|
2521
|
-
|
|
2522
|
-
/**
|
|
2523
|
-
* A mixin to provide `pattern` property.
|
|
2524
|
-
*
|
|
2525
|
-
* @polymerMixin
|
|
2526
|
-
* @mixes InputConstraintsMixin
|
|
2527
|
-
*/
|
|
2528
|
-
const PatternMixin = (superclass) =>
|
|
2529
|
-
class PatternMixinClass extends InputConstraintsMixin(superclass) {
|
|
2530
|
-
static get properties() {
|
|
2531
|
-
return {
|
|
2532
|
-
/**
|
|
2533
|
-
* A regular expression that the value is checked against.
|
|
2534
|
-
* The pattern must match the entire value, not just some subset.
|
|
2535
|
-
*/
|
|
2536
|
-
pattern: {
|
|
2537
|
-
type: String,
|
|
2538
|
-
},
|
|
2539
|
-
};
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
static get delegateAttrs() {
|
|
2543
|
-
return [...super.delegateAttrs, 'pattern'];
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
static get constraints() {
|
|
2547
|
-
return [...super.constraints, 'pattern'];
|
|
2548
|
-
}
|
|
2549
|
-
};
|
|
2550
|
-
|
|
2551
|
-
/**
|
|
2552
|
-
* @license
|
|
2553
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
2554
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
2555
|
-
*/
|
|
2556
|
-
|
|
2557
|
-
/**
|
|
2558
|
-
* @polymerMixin
|
|
2559
|
-
*/
|
|
2560
|
-
const ComboBoxDataProviderMixin = (superClass) =>
|
|
2561
|
-
class DataProviderMixin extends superClass {
|
|
2562
|
-
static get properties() {
|
|
2563
|
-
return {
|
|
2564
|
-
/**
|
|
2565
|
-
* Number of items fetched at a time from the dataprovider.
|
|
2566
|
-
* @attr {number} page-size
|
|
2567
|
-
* @type {number}
|
|
2568
|
-
*/
|
|
2569
|
-
pageSize: {
|
|
2570
|
-
type: Number,
|
|
2571
|
-
value: 50,
|
|
2572
|
-
observer: '_pageSizeChanged',
|
|
2573
|
-
},
|
|
2574
|
-
|
|
2575
|
-
/**
|
|
2576
|
-
* Total number of items.
|
|
2577
|
-
* @type {number | undefined}
|
|
2578
|
-
*/
|
|
2579
|
-
size: {
|
|
2580
|
-
type: Number,
|
|
2581
|
-
observer: '_sizeChanged',
|
|
2582
|
-
},
|
|
2583
|
-
|
|
2584
|
-
/**
|
|
2585
|
-
* Function that provides items lazily. Receives arguments `params`, `callback`
|
|
2586
|
-
*
|
|
2587
|
-
* `params.page` Requested page index
|
|
2588
|
-
*
|
|
2589
|
-
* `params.pageSize` Current page size
|
|
2590
|
-
*
|
|
2591
|
-
* `params.filter` Currently applied filter
|
|
2592
|
-
*
|
|
2593
|
-
* `callback(items, size)` Callback function with arguments:
|
|
2594
|
-
* - `items` Current page of items
|
|
2595
|
-
* - `size` Total number of items.
|
|
2596
|
-
* @type {ComboBoxDataProvider | undefined}
|
|
2597
|
-
*/
|
|
2598
|
-
dataProvider: {
|
|
2599
|
-
type: Object,
|
|
2600
|
-
observer: '_dataProviderChanged',
|
|
2601
|
-
},
|
|
2602
|
-
|
|
2603
|
-
/** @private */
|
|
2604
|
-
_pendingRequests: {
|
|
2605
|
-
value: () => {
|
|
2606
|
-
return {};
|
|
2607
|
-
},
|
|
2608
|
-
},
|
|
2609
|
-
|
|
2610
|
-
/** @private */
|
|
2611
|
-
__placeHolder: {
|
|
2612
|
-
value: new ComboBoxPlaceholder(),
|
|
2613
|
-
},
|
|
2614
|
-
|
|
2615
|
-
/** @private */
|
|
2616
|
-
__previousDataProviderFilter: {
|
|
2617
|
-
type: String,
|
|
2618
|
-
},
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
static get observers() {
|
|
2623
|
-
return [
|
|
2624
|
-
'_dataProviderFilterChanged(filter)',
|
|
2625
|
-
'_warnDataProviderValue(dataProvider, value)',
|
|
2626
|
-
'_ensureFirstPage(opened)',
|
|
2627
|
-
];
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
/** @protected */
|
|
2631
|
-
ready() {
|
|
2632
|
-
super.ready();
|
|
2633
|
-
this._scroller.addEventListener('index-requested', (e) => {
|
|
2634
|
-
const index = e.detail.index;
|
|
2635
|
-
const currentScrollerPos = e.detail.currentScrollerPos;
|
|
2636
|
-
const allowedIndexRange = Math.floor(this.pageSize * 1.5);
|
|
2637
|
-
|
|
2638
|
-
// Ignores the indexes, which are being re-sent during scrolling reset,
|
|
2639
|
-
// if the corresponding page is around the current scroller position.
|
|
2640
|
-
// Otherwise, there might be a last pages duplicates, which cause the
|
|
2641
|
-
// loading indicator hanging and blank items
|
|
2642
|
-
if (this._shouldSkipIndex(index, allowedIndexRange, currentScrollerPos)) {
|
|
2643
|
-
return;
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
if (index !== undefined) {
|
|
2647
|
-
const page = this._getPageForIndex(index);
|
|
2648
|
-
if (this._shouldLoadPage(page)) {
|
|
2649
|
-
this._loadPage(page);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
});
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
/** @private */
|
|
2656
|
-
_dataProviderFilterChanged(filter) {
|
|
2657
|
-
if (this.__previousDataProviderFilter === undefined && filter === '') {
|
|
2658
|
-
this.__previousDataProviderFilter = filter;
|
|
2659
|
-
return;
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2662
|
-
if (this.__previousDataProviderFilter !== filter) {
|
|
2663
|
-
this.__previousDataProviderFilter = filter;
|
|
2664
|
-
|
|
2665
|
-
this._pendingRequests = {};
|
|
2666
|
-
// Immediately mark as loading if this refresh leads to re-fetching pages
|
|
2667
|
-
// This prevents some issues with the properties below triggering
|
|
2668
|
-
// observers that also rely on the loading state
|
|
2669
|
-
this.loading = this._shouldFetchData();
|
|
2670
|
-
// Reset size and internal loading state
|
|
2671
|
-
this.size = undefined;
|
|
2672
|
-
|
|
2673
|
-
this.clearCache();
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
/** @private */
|
|
2678
|
-
_shouldFetchData() {
|
|
2679
|
-
if (!this.dataProvider) {
|
|
2680
|
-
return false;
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
return this.opened || (this.filter && this.filter.length);
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
/** @private */
|
|
2687
|
-
_ensureFirstPage(opened) {
|
|
2688
|
-
if (opened && this._shouldLoadPage(0)) {
|
|
2689
|
-
this._loadPage(0);
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
/** @private */
|
|
2694
|
-
_shouldSkipIndex(index, allowedIndexRange, currentScrollerPos) {
|
|
2695
|
-
return (
|
|
2696
|
-
currentScrollerPos !== 0 &&
|
|
2697
|
-
index >= currentScrollerPos - allowedIndexRange &&
|
|
2698
|
-
index <= currentScrollerPos + allowedIndexRange
|
|
2699
|
-
);
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
/** @private */
|
|
2703
|
-
_shouldLoadPage(page) {
|
|
2704
|
-
if (!this.filteredItems || this._forceNextRequest) {
|
|
2705
|
-
this._forceNextRequest = false;
|
|
2706
|
-
return true;
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
const loadedItem = this.filteredItems[page * this.pageSize];
|
|
2710
|
-
if (loadedItem !== undefined) {
|
|
2711
|
-
return loadedItem instanceof ComboBoxPlaceholder;
|
|
2712
|
-
}
|
|
2713
|
-
return this.size === undefined;
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
/** @private */
|
|
2717
|
-
_loadPage(page) {
|
|
2718
|
-
// Make sure same page isn't requested multiple times.
|
|
2719
|
-
if (this._pendingRequests[page] || !this.dataProvider) {
|
|
2720
|
-
return;
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
const params = {
|
|
2724
|
-
page,
|
|
2725
|
-
pageSize: this.pageSize,
|
|
2726
|
-
filter: this.filter,
|
|
2727
|
-
};
|
|
2728
|
-
|
|
2729
|
-
const callback = (items, size) => {
|
|
2730
|
-
if (this._pendingRequests[page] !== callback) {
|
|
2731
|
-
return;
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
const filteredItems = this.filteredItems ? [...this.filteredItems] : [];
|
|
2735
|
-
filteredItems.splice(params.page * params.pageSize, items.length, ...items);
|
|
2736
|
-
this.filteredItems = filteredItems;
|
|
2737
|
-
|
|
2738
|
-
if (!this.opened && !this._isInputFocused()) {
|
|
2739
|
-
this._commitValue();
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
if (size !== undefined) {
|
|
2743
|
-
this.size = size;
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
delete this._pendingRequests[page];
|
|
2747
|
-
|
|
2748
|
-
if (Object.keys(this._pendingRequests).length === 0) {
|
|
2749
|
-
this.loading = false;
|
|
2750
|
-
}
|
|
2751
|
-
};
|
|
2752
|
-
|
|
2753
|
-
this._pendingRequests[page] = callback;
|
|
2754
|
-
// Set the `loading` flag only after marking the request as pending
|
|
2755
|
-
// to prevent the same page from getting requested multiple times
|
|
2756
|
-
// as a result of `__loadingChanged` in the scroller which requests
|
|
2757
|
-
// a virtualizer update which in turn may trigger a data provider page request.
|
|
2758
|
-
this.loading = true;
|
|
2759
|
-
this.dataProvider(params, callback);
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
|
-
/** @private */
|
|
2763
|
-
_getPageForIndex(index) {
|
|
2764
|
-
return Math.floor(index / this.pageSize);
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
/**
|
|
2768
|
-
* Clears the cached pages and reloads data from dataprovider when needed.
|
|
2769
|
-
*/
|
|
2770
|
-
clearCache() {
|
|
2771
|
-
if (!this.dataProvider) {
|
|
2772
|
-
return;
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
this._pendingRequests = {};
|
|
2776
|
-
const filteredItems = [];
|
|
2777
|
-
for (let i = 0; i < (this.size || 0); i++) {
|
|
2778
|
-
filteredItems.push(this.__placeHolder);
|
|
2779
|
-
}
|
|
2780
|
-
this.filteredItems = filteredItems;
|
|
2781
|
-
|
|
2782
|
-
if (this._shouldFetchData()) {
|
|
2783
|
-
this._forceNextRequest = false;
|
|
2784
|
-
this._loadPage(0);
|
|
2785
|
-
} else {
|
|
2786
|
-
this._forceNextRequest = true;
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
/** @private */
|
|
2791
|
-
_sizeChanged(size = 0) {
|
|
2792
|
-
const filteredItems = (this.filteredItems || []).slice(0, size);
|
|
2793
|
-
for (let i = 0; i < size; i++) {
|
|
2794
|
-
filteredItems[i] = filteredItems[i] !== undefined ? filteredItems[i] : this.__placeHolder;
|
|
2795
|
-
}
|
|
2796
|
-
this.filteredItems = filteredItems;
|
|
2797
|
-
|
|
2798
|
-
// Cleans up the redundant pending requests for pages > size
|
|
2799
|
-
// Refers to https://github.com/vaadin/vaadin-flow-components/issues/229
|
|
2800
|
-
this._flushPendingRequests(size);
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
/** @private */
|
|
2804
|
-
_pageSizeChanged(pageSize, oldPageSize) {
|
|
2805
|
-
if (Math.floor(pageSize) !== pageSize || pageSize < 1) {
|
|
2806
|
-
this.pageSize = oldPageSize;
|
|
2807
|
-
throw new Error('`pageSize` value must be an integer > 0');
|
|
2808
|
-
}
|
|
2809
|
-
this.clearCache();
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
/** @private */
|
|
2813
|
-
_dataProviderChanged(dataProvider, oldDataProvider) {
|
|
2814
|
-
this._ensureItemsOrDataProvider(() => {
|
|
2815
|
-
this.dataProvider = oldDataProvider;
|
|
2816
|
-
});
|
|
2817
|
-
|
|
2818
|
-
this.clearCache();
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
/** @private */
|
|
2822
|
-
_ensureItemsOrDataProvider(restoreOldValueCallback) {
|
|
2823
|
-
if (this.items !== undefined && this.dataProvider !== undefined) {
|
|
2824
|
-
restoreOldValueCallback();
|
|
2825
|
-
throw new Error('Using `items` and `dataProvider` together is not supported');
|
|
2826
|
-
} else if (this.dataProvider && !this.filteredItems) {
|
|
2827
|
-
this.filteredItems = [];
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
/** @private */
|
|
2832
|
-
_warnDataProviderValue(dataProvider, value) {
|
|
2833
|
-
if (dataProvider && value !== '' && (this.selectedItem === undefined || this.selectedItem === null)) {
|
|
2834
|
-
const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
|
|
2835
|
-
if (valueIndex < 0 || !this._getItemLabel(this.filteredItems[valueIndex])) {
|
|
2836
|
-
console.warn(
|
|
2837
|
-
'Warning: unable to determine the label for the provided `value`. ' +
|
|
2838
|
-
'Nothing to display in the text field. This usually happens when ' +
|
|
2839
|
-
'setting an initial `value` before any items are returned from ' +
|
|
2840
|
-
'the `dataProvider` callback. Consider setting `selectedItem` ' +
|
|
2841
|
-
'instead of `value`',
|
|
2842
|
-
);
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
/**
|
|
2848
|
-
* This method cleans up the page callbacks which refers to the
|
|
2849
|
-
* non-existing pages, i.e. which item indexes are greater than the
|
|
2850
|
-
* changed size.
|
|
2851
|
-
* This case is basically happens when:
|
|
2852
|
-
* 1. Users scroll fast to the bottom and combo box generates the
|
|
2853
|
-
* redundant page request/callback
|
|
2854
|
-
* 2. Server side uses undefined size lazy loading and suddenly reaches
|
|
2855
|
-
* the exact size which is on the range edge
|
|
2856
|
-
* (for default page size = 50, it will be 100, 200, 300, ...).
|
|
2857
|
-
* @param size the new size of items
|
|
2858
|
-
* @private
|
|
2859
|
-
*/
|
|
2860
|
-
_flushPendingRequests(size) {
|
|
2861
|
-
if (this._pendingRequests) {
|
|
2862
|
-
const lastPage = Math.ceil(size / this.pageSize);
|
|
2863
|
-
Object.entries(this._pendingRequests).forEach(([page, callback]) => {
|
|
2864
|
-
if (parseInt(page) >= lastPage) {
|
|
2865
|
-
callback([], size);
|
|
2866
|
-
}
|
|
2867
|
-
});
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
};
|
|
2871
|
-
|
|
2872
|
-
/**
|
|
2873
|
-
* @license
|
|
2874
|
-
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
2875
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
2876
|
-
*/
|
|
2877
|
-
|
|
2878
|
-
/**
|
|
2879
|
-
* Passes the component to the template renderer callback if the template renderer is imported.
|
|
2880
|
-
* Otherwise, if there is a template, it warns that the template renderer needs to be imported.
|
|
2881
|
-
*
|
|
2882
|
-
* @param {HTMLElement} component
|
|
2883
|
-
*/
|
|
2884
|
-
function processTemplates(component) {
|
|
2885
|
-
if (window.Vaadin && window.Vaadin.templateRendererCallback) {
|
|
2886
|
-
window.Vaadin.templateRendererCallback(component);
|
|
2887
|
-
return;
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
if (component.querySelector('template')) {
|
|
2891
|
-
console.warn(
|
|
2892
|
-
`WARNING: <template> inside <${component.localName}> is no longer supported. Import @vaadin/polymer-legacy-adapter/template-renderer.js to enable compatibility.`,
|
|
2893
|
-
);
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
/**
|
|
2898
|
-
* @license
|
|
2899
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
2900
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
2901
|
-
*/
|
|
2902
|
-
|
|
2903
|
-
/**
|
|
2904
|
-
* Checks if the value is supported as an item value in this control.
|
|
2905
|
-
*
|
|
2906
|
-
* @param {unknown} value
|
|
2907
|
-
* @return {boolean}
|
|
2908
|
-
*/
|
|
2909
|
-
function isValidValue(value) {
|
|
2910
|
-
return value !== undefined && value !== null;
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
/**
|
|
2914
|
-
* Returns the index of the first item that satisfies the provided testing function
|
|
2915
|
-
* ignoring placeholder items.
|
|
2916
|
-
*
|
|
2917
|
-
* @param {Array<ComboBoxItem | string>} items
|
|
2918
|
-
* @param {Function} callback
|
|
2919
|
-
* @return {number}
|
|
2920
|
-
*/
|
|
2921
|
-
function findItemIndex(items, callback) {
|
|
2922
|
-
return items.findIndex((item) => {
|
|
2923
|
-
if (item instanceof ComboBoxPlaceholder) {
|
|
2924
|
-
return false;
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
return callback(item);
|
|
2928
|
-
});
|
|
2929
|
-
}
|
|
2930
|
-
|
|
2931
|
-
/**
|
|
2932
|
-
* @polymerMixin
|
|
2933
|
-
* @mixes ControllerMixin
|
|
2934
|
-
* @mixes ValidateMixin
|
|
2935
|
-
* @mixes DisabledMixin
|
|
2936
|
-
* @mixes InputMixin
|
|
2937
|
-
* @mixes KeyboardMixin
|
|
2938
|
-
* @mixes FocusMixin
|
|
2939
|
-
* @mixes OverlayClassMixin
|
|
2940
|
-
* @param {function(new:HTMLElement)} subclass
|
|
2941
|
-
*/
|
|
2942
|
-
const ComboBoxMixin = (subclass) =>
|
|
2943
|
-
class ComboBoxMixinClass extends OverlayClassMixin(
|
|
2944
|
-
ControllerMixin(ValidateMixin(FocusMixin(KeyboardMixin(InputMixin(DisabledMixin(subclass)))))),
|
|
2945
|
-
) {
|
|
2946
|
-
static get properties() {
|
|
2947
|
-
return {
|
|
2948
|
-
/**
|
|
2949
|
-
* True if the dropdown is open, false otherwise.
|
|
2950
|
-
* @type {boolean}
|
|
2951
|
-
*/
|
|
2952
|
-
opened: {
|
|
2953
|
-
type: Boolean,
|
|
2954
|
-
notify: true,
|
|
2955
|
-
value: false,
|
|
2956
|
-
reflectToAttribute: true,
|
|
2957
|
-
observer: '_openedChanged',
|
|
2958
|
-
},
|
|
2959
|
-
|
|
2960
|
-
/**
|
|
2961
|
-
* Set true to prevent the overlay from opening automatically.
|
|
2962
|
-
* @attr {boolean} auto-open-disabled
|
|
2963
|
-
*/
|
|
2964
|
-
autoOpenDisabled: {
|
|
2965
|
-
type: Boolean,
|
|
2966
|
-
},
|
|
2967
|
-
|
|
2968
|
-
/**
|
|
2969
|
-
* When present, it specifies that the field is read-only.
|
|
2970
|
-
* @type {boolean}
|
|
2971
|
-
*/
|
|
2972
|
-
readonly: {
|
|
2973
|
-
type: Boolean,
|
|
2974
|
-
value: false,
|
|
2975
|
-
reflectToAttribute: true,
|
|
2976
|
-
},
|
|
2977
|
-
|
|
2978
|
-
/**
|
|
2979
|
-
* Custom function for rendering the content of every item.
|
|
2980
|
-
* Receives three arguments:
|
|
2981
|
-
*
|
|
2982
|
-
* - `root` The `<vaadin-combo-box-item>` internal container DOM element.
|
|
2983
|
-
* - `comboBox` The reference to the `<vaadin-combo-box>` element.
|
|
2984
|
-
* - `model` The object with the properties related with the rendered
|
|
2985
|
-
* item, contains:
|
|
2986
|
-
* - `model.index` The index of the rendered item.
|
|
2987
|
-
* - `model.item` The item.
|
|
2988
|
-
* @type {ComboBoxRenderer | undefined}
|
|
2989
|
-
*/
|
|
2990
|
-
renderer: Function,
|
|
2991
|
-
|
|
2992
|
-
/**
|
|
2993
|
-
* A full set of items to filter the visible options from.
|
|
2994
|
-
* The items can be of either `String` or `Object` type.
|
|
2995
|
-
* @type {!Array<!ComboBoxItem | string> | undefined}
|
|
2996
|
-
*/
|
|
2997
|
-
items: {
|
|
2998
|
-
type: Array,
|
|
2999
|
-
observer: '_itemsChanged',
|
|
3000
|
-
},
|
|
3001
|
-
|
|
3002
|
-
/**
|
|
3003
|
-
* If `true`, the user can input a value that is not present in the items list.
|
|
3004
|
-
* `value` property will be set to the input value in this case.
|
|
3005
|
-
* Also, when `value` is set programmatically, the input value will be set
|
|
3006
|
-
* to reflect that value.
|
|
3007
|
-
* @attr {boolean} allow-custom-value
|
|
3008
|
-
* @type {boolean}
|
|
3009
|
-
*/
|
|
3010
|
-
allowCustomValue: {
|
|
3011
|
-
type: Boolean,
|
|
3012
|
-
value: false,
|
|
3013
|
-
},
|
|
3014
|
-
|
|
3015
|
-
/**
|
|
3016
|
-
* A subset of items, filtered based on the user input. Filtered items
|
|
3017
|
-
* can be assigned directly to omit the internal filtering functionality.
|
|
3018
|
-
* The items can be of either `String` or `Object` type.
|
|
3019
|
-
* @type {!Array<!ComboBoxItem | string> | undefined}
|
|
3020
|
-
*/
|
|
3021
|
-
filteredItems: {
|
|
3022
|
-
type: Array,
|
|
3023
|
-
observer: '_filteredItemsChanged',
|
|
3024
|
-
},
|
|
3025
|
-
|
|
3026
|
-
/**
|
|
3027
|
-
* Used to detect user value changes and fire `change` events.
|
|
3028
|
-
* @private
|
|
3029
|
-
*/
|
|
3030
|
-
_lastCommittedValue: String,
|
|
3031
|
-
|
|
3032
|
-
/**
|
|
3033
|
-
* When set to `true`, "loading" attribute is added to host and the overlay element.
|
|
3034
|
-
* @type {boolean}
|
|
3035
|
-
*/
|
|
3036
|
-
loading: {
|
|
3037
|
-
type: Boolean,
|
|
3038
|
-
value: false,
|
|
3039
|
-
reflectToAttribute: true,
|
|
3040
|
-
},
|
|
3041
|
-
|
|
3042
|
-
/**
|
|
3043
|
-
* @type {number}
|
|
3044
|
-
* @protected
|
|
3045
|
-
*/
|
|
3046
|
-
_focusedIndex: {
|
|
3047
|
-
type: Number,
|
|
3048
|
-
observer: '_focusedIndexChanged',
|
|
3049
|
-
value: -1,
|
|
3050
|
-
},
|
|
3051
|
-
|
|
3052
|
-
/**
|
|
3053
|
-
* Filtering string the user has typed into the input field.
|
|
3054
|
-
* @type {string}
|
|
3055
|
-
*/
|
|
3056
|
-
filter: {
|
|
3057
|
-
type: String,
|
|
3058
|
-
value: '',
|
|
3059
|
-
notify: true,
|
|
3060
|
-
},
|
|
3061
|
-
|
|
3062
|
-
/**
|
|
3063
|
-
* The selected item from the `items` array.
|
|
3064
|
-
* @type {ComboBoxItem | string | undefined}
|
|
3065
|
-
*/
|
|
3066
|
-
selectedItem: {
|
|
3067
|
-
type: Object,
|
|
3068
|
-
notify: true,
|
|
3069
|
-
},
|
|
3070
|
-
|
|
3071
|
-
/**
|
|
3072
|
-
* Path for label of the item. If `items` is an array of objects, the
|
|
3073
|
-
* `itemLabelPath` is used to fetch the displayed string label for each
|
|
3074
|
-
* item.
|
|
3075
|
-
*
|
|
3076
|
-
* The item label is also used for matching items when processing user
|
|
3077
|
-
* input, i.e., for filtering and selecting items.
|
|
3078
|
-
* @attr {string} item-label-path
|
|
3079
|
-
* @type {string}
|
|
3080
|
-
*/
|
|
3081
|
-
itemLabelPath: {
|
|
3082
|
-
type: String,
|
|
3083
|
-
value: 'label',
|
|
3084
|
-
observer: '_itemLabelPathChanged',
|
|
3085
|
-
},
|
|
3086
|
-
|
|
3087
|
-
/**
|
|
3088
|
-
* Path for the value of the item. If `items` is an array of objects, the
|
|
3089
|
-
* `itemValuePath:` is used to fetch the string value for the selected
|
|
3090
|
-
* item.
|
|
3091
|
-
*
|
|
3092
|
-
* The item value is used in the `value` property of the combo box,
|
|
3093
|
-
* to provide the form value.
|
|
3094
|
-
* @attr {string} item-value-path
|
|
3095
|
-
* @type {string}
|
|
3096
|
-
*/
|
|
3097
|
-
itemValuePath: {
|
|
3098
|
-
type: String,
|
|
3099
|
-
value: 'value',
|
|
3100
|
-
},
|
|
3101
|
-
|
|
3102
|
-
/**
|
|
3103
|
-
* Path for the id of the item. If `items` is an array of objects,
|
|
3104
|
-
* the `itemIdPath` is used to compare and identify the same item
|
|
3105
|
-
* in `selectedItem` and `filteredItems` (items given by the
|
|
3106
|
-
* `dataProvider` callback).
|
|
3107
|
-
* @attr {string} item-id-path
|
|
3108
|
-
*/
|
|
3109
|
-
itemIdPath: String,
|
|
3110
|
-
|
|
3111
|
-
/**
|
|
3112
|
-
* @type {!HTMLElement | undefined}
|
|
3113
|
-
* @protected
|
|
3114
|
-
*/
|
|
3115
|
-
_toggleElement: {
|
|
3116
|
-
type: Object,
|
|
3117
|
-
observer: '_toggleElementChanged',
|
|
3118
|
-
},
|
|
3119
|
-
|
|
3120
|
-
/**
|
|
3121
|
-
* Set of items to be rendered in the dropdown.
|
|
3122
|
-
* @protected
|
|
3123
|
-
*/
|
|
3124
|
-
_dropdownItems: {
|
|
3125
|
-
type: Array,
|
|
3126
|
-
},
|
|
3127
|
-
|
|
3128
|
-
/** @private */
|
|
3129
|
-
_closeOnBlurIsPrevented: Boolean,
|
|
3130
|
-
|
|
3131
|
-
/** @private */
|
|
3132
|
-
_scroller: Object,
|
|
3133
|
-
|
|
3134
|
-
/** @private */
|
|
3135
|
-
_overlayOpened: {
|
|
3136
|
-
type: Boolean,
|
|
3137
|
-
observer: '_overlayOpenedChanged',
|
|
3138
|
-
},
|
|
3139
|
-
};
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
static get observers() {
|
|
3143
|
-
return [
|
|
3144
|
-
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
|
|
3145
|
-
'_openedOrItemsChanged(opened, _dropdownItems, loading)',
|
|
3146
|
-
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
|
|
3147
|
-
];
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
constructor() {
|
|
3151
|
-
super();
|
|
3152
|
-
this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
|
|
3153
|
-
this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
|
|
3154
|
-
this._boundOnClick = this._onClick.bind(this);
|
|
3155
|
-
this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
|
|
3156
|
-
this._boundOnTouchend = this._onTouchend.bind(this);
|
|
3157
|
-
}
|
|
3158
|
-
|
|
3159
|
-
/**
|
|
3160
|
-
* Tag name prefix used by scroller and items.
|
|
3161
|
-
* @protected
|
|
3162
|
-
* @return {string}
|
|
3163
|
-
*/
|
|
3164
|
-
get _tagNamePrefix() {
|
|
3165
|
-
return 'vaadin-combo-box';
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
/**
|
|
3169
|
-
* Get a reference to the native `<input>` element.
|
|
3170
|
-
* Override to provide a custom input.
|
|
3171
|
-
* @protected
|
|
3172
|
-
* @return {HTMLInputElement | undefined}
|
|
3173
|
-
*/
|
|
3174
|
-
get _nativeInput() {
|
|
3175
|
-
return this.inputElement;
|
|
3176
|
-
}
|
|
3177
|
-
|
|
3178
|
-
/**
|
|
3179
|
-
* Override method inherited from `InputMixin`
|
|
3180
|
-
* to customize the input element.
|
|
3181
|
-
* @protected
|
|
3182
|
-
* @override
|
|
3183
|
-
*/
|
|
3184
|
-
_inputElementChanged(inputElement) {
|
|
3185
|
-
super._inputElementChanged(inputElement);
|
|
3186
|
-
|
|
3187
|
-
const input = this._nativeInput;
|
|
3188
|
-
|
|
3189
|
-
if (input) {
|
|
3190
|
-
input.autocomplete = 'off';
|
|
3191
|
-
input.autocapitalize = 'off';
|
|
3192
|
-
|
|
3193
|
-
input.setAttribute('role', 'combobox');
|
|
3194
|
-
input.setAttribute('aria-autocomplete', 'list');
|
|
3195
|
-
input.setAttribute('aria-expanded', !!this.opened);
|
|
3196
|
-
|
|
3197
|
-
// Disable the macOS Safari spell check auto corrections.
|
|
3198
|
-
input.setAttribute('spellcheck', 'false');
|
|
3199
|
-
|
|
3200
|
-
// Disable iOS autocorrect suggestions.
|
|
3201
|
-
input.setAttribute('autocorrect', 'off');
|
|
3202
|
-
|
|
3203
|
-
this._revertInputValueToValue();
|
|
3204
|
-
|
|
3205
|
-
if (this.clearElement) {
|
|
3206
|
-
this.clearElement.addEventListener('mousedown', this._boundOnClearButtonMouseDown);
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
/** @protected */
|
|
3212
|
-
ready() {
|
|
3213
|
-
super.ready();
|
|
3214
|
-
|
|
3215
|
-
this._initOverlay();
|
|
3216
|
-
this._initScroller();
|
|
3217
|
-
|
|
3218
|
-
this._lastCommittedValue = this.value;
|
|
3219
|
-
|
|
3220
|
-
this.addEventListener('click', this._boundOnClick);
|
|
3221
|
-
this.addEventListener('touchend', this._boundOnTouchend);
|
|
3222
|
-
|
|
3223
|
-
const bringToFrontListener = () => {
|
|
3224
|
-
requestAnimationFrame(() => {
|
|
3225
|
-
this._overlayElement.bringToFront();
|
|
3226
|
-
});
|
|
3227
|
-
};
|
|
3228
|
-
|
|
3229
|
-
this.addEventListener('mousedown', bringToFrontListener);
|
|
3230
|
-
this.addEventListener('touchstart', bringToFrontListener);
|
|
3231
|
-
|
|
3232
|
-
processTemplates(this);
|
|
3233
|
-
|
|
3234
|
-
this.addController(new VirtualKeyboardController(this));
|
|
3235
|
-
}
|
|
3236
|
-
|
|
3237
|
-
/** @protected */
|
|
3238
|
-
disconnectedCallback() {
|
|
3239
|
-
super.disconnectedCallback();
|
|
3240
|
-
|
|
3241
|
-
// Close the overlay on detach
|
|
3242
|
-
this.close();
|
|
3243
|
-
}
|
|
3244
|
-
|
|
3245
|
-
/**
|
|
3246
|
-
* Requests an update for the content of items.
|
|
3247
|
-
* While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
|
|
3248
|
-
*
|
|
3249
|
-
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
|
|
3250
|
-
*/
|
|
3251
|
-
requestContentUpdate() {
|
|
3252
|
-
if (!this._scroller) {
|
|
3253
|
-
return;
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
this._scroller.requestContentUpdate();
|
|
3257
|
-
|
|
3258
|
-
this._getItemElements().forEach((item) => {
|
|
3259
|
-
item.requestContentUpdate();
|
|
3260
|
-
});
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
/**
|
|
3264
|
-
* Opens the dropdown list.
|
|
3265
|
-
*/
|
|
3266
|
-
open() {
|
|
3267
|
-
// Prevent _open() being called when input is disabled or read-only
|
|
3268
|
-
if (!this.disabled && !this.readonly) {
|
|
3269
|
-
this.opened = true;
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
/**
|
|
3274
|
-
* Closes the dropdown list.
|
|
3275
|
-
*/
|
|
3276
|
-
close() {
|
|
3277
|
-
this.opened = false;
|
|
3278
|
-
}
|
|
3279
|
-
|
|
3280
|
-
/**
|
|
3281
|
-
* Override Polymer lifecycle callback to handle `filter` property change after
|
|
3282
|
-
* the observer for `opened` property is triggered. This is needed when opening
|
|
3283
|
-
* combo-box on user input to ensure the focused index is set correctly.
|
|
3284
|
-
*
|
|
3285
|
-
* @param {!Object} currentProps Current accessor values
|
|
3286
|
-
* @param {?Object} changedProps Properties changed since the last call
|
|
3287
|
-
* @param {?Object} oldProps Previous values for each changed property
|
|
3288
|
-
* @protected
|
|
3289
|
-
* @override
|
|
3290
|
-
*/
|
|
3291
|
-
_propertiesChanged(currentProps, changedProps, oldProps) {
|
|
3292
|
-
super._propertiesChanged(currentProps, changedProps, oldProps);
|
|
3293
|
-
|
|
3294
|
-
if (changedProps.filter !== undefined) {
|
|
3295
|
-
this._filterChanged(changedProps.filter);
|
|
3296
|
-
}
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
/** @private */
|
|
3300
|
-
_initOverlay() {
|
|
3301
|
-
const overlay = this.$.overlay;
|
|
3302
|
-
|
|
3303
|
-
// Store instance for detecting "dir" attribute on opening
|
|
3304
|
-
overlay._comboBox = this;
|
|
3305
|
-
|
|
3306
|
-
overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
|
|
3307
|
-
overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
|
|
3308
|
-
|
|
3309
|
-
// Prevent blurring the input when clicking inside the overlay
|
|
3310
|
-
overlay.addEventListener('mousedown', (e) => e.preventDefault());
|
|
3311
|
-
|
|
3312
|
-
// Manual two-way binding for the overlay "opened" property
|
|
3313
|
-
overlay.addEventListener('opened-changed', (e) => {
|
|
3314
|
-
this._overlayOpened = e.detail.value;
|
|
3315
|
-
});
|
|
3316
|
-
|
|
3317
|
-
this._overlayElement = overlay;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
/**
|
|
3321
|
-
* Create and initialize the scroller element.
|
|
3322
|
-
* Override to provide custom host reference.
|
|
3323
|
-
*
|
|
3324
|
-
* @protected
|
|
3325
|
-
*/
|
|
3326
|
-
_initScroller(host) {
|
|
3327
|
-
const scrollerTag = `${this._tagNamePrefix}-scroller`;
|
|
3328
|
-
|
|
3329
|
-
const overlay = this._overlayElement;
|
|
3330
|
-
|
|
3331
|
-
overlay.renderer = (root) => {
|
|
3332
|
-
if (!root.firstChild) {
|
|
3333
|
-
root.appendChild(document.createElement(scrollerTag));
|
|
3334
|
-
}
|
|
3335
|
-
};
|
|
3336
|
-
|
|
3337
|
-
// Ensure the scroller is rendered
|
|
3338
|
-
overlay.requestContentUpdate();
|
|
3339
|
-
|
|
3340
|
-
const scroller = overlay.querySelector(scrollerTag);
|
|
3341
|
-
|
|
3342
|
-
scroller.owner = host || this;
|
|
3343
|
-
scroller.getItemLabel = this._getItemLabel.bind(this);
|
|
3344
|
-
scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
|
|
3345
|
-
|
|
3346
|
-
// Trigger the observer to set properties
|
|
3347
|
-
this._scroller = scroller;
|
|
3348
|
-
}
|
|
3349
|
-
|
|
3350
|
-
/** @private */
|
|
3351
|
-
// eslint-disable-next-line max-params
|
|
3352
|
-
_updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
|
|
3353
|
-
if (scroller) {
|
|
3354
|
-
if (opened) {
|
|
3355
|
-
scroller.style.maxHeight =
|
|
3356
|
-
getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
scroller.setProperties({
|
|
3360
|
-
items: opened ? items : [],
|
|
3361
|
-
opened,
|
|
3362
|
-
loading,
|
|
3363
|
-
selectedItem,
|
|
3364
|
-
itemIdPath,
|
|
3365
|
-
focusedIndex,
|
|
3366
|
-
renderer,
|
|
3367
|
-
theme,
|
|
3368
|
-
});
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
/** @private */
|
|
3373
|
-
_openedOrItemsChanged(opened, items, loading) {
|
|
3374
|
-
// Close the overlay if there are no items to display.
|
|
3375
|
-
// See https://github.com/vaadin/vaadin-combo-box/pull/964
|
|
3376
|
-
this._overlayOpened = !!(opened && (loading || (items && items.length)));
|
|
3377
|
-
}
|
|
3378
|
-
|
|
3379
|
-
/** @private */
|
|
3380
|
-
_overlayOpenedChanged(opened, wasOpened) {
|
|
3381
|
-
if (opened) {
|
|
3382
|
-
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
|
|
3383
|
-
|
|
3384
|
-
this._onOpened();
|
|
3385
|
-
} else if (wasOpened && this._dropdownItems && this._dropdownItems.length) {
|
|
3386
|
-
this.close();
|
|
3387
|
-
|
|
3388
|
-
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
|
|
3389
|
-
}
|
|
3390
|
-
}
|
|
3391
|
-
|
|
3392
|
-
/** @private */
|
|
3393
|
-
_focusedIndexChanged(index, oldIndex) {
|
|
3394
|
-
if (oldIndex === undefined) {
|
|
3395
|
-
return;
|
|
3396
|
-
}
|
|
3397
|
-
this._updateActiveDescendant(index);
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
/** @protected */
|
|
3401
|
-
_isInputFocused() {
|
|
3402
|
-
return this.inputElement && isElementFocused(this.inputElement);
|
|
3403
|
-
}
|
|
3404
|
-
|
|
3405
|
-
/** @private */
|
|
3406
|
-
_updateActiveDescendant(index) {
|
|
3407
|
-
const input = this._nativeInput;
|
|
3408
|
-
if (!input) {
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3411
|
-
|
|
3412
|
-
const item = this._getItemElements().find((el) => el.index === index);
|
|
3413
|
-
if (item) {
|
|
3414
|
-
input.setAttribute('aria-activedescendant', item.id);
|
|
3415
|
-
} else {
|
|
3416
|
-
input.removeAttribute('aria-activedescendant');
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
/** @private */
|
|
3421
|
-
_openedChanged(opened, wasOpened) {
|
|
3422
|
-
// Prevent _close() being called when opened is set to its default value (false).
|
|
3423
|
-
if (wasOpened === undefined) {
|
|
3424
|
-
return;
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
if (opened) {
|
|
3428
|
-
this._openedWithFocusRing = this.hasAttribute('focus-ring');
|
|
3429
|
-
// For touch devices, we don't want to popup virtual keyboard
|
|
3430
|
-
// unless input element is explicitly focused by the user.
|
|
3431
|
-
if (!this._isInputFocused() && !isTouch) {
|
|
3432
|
-
if (this.inputElement) {
|
|
3433
|
-
this.inputElement.focus();
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
|
|
3437
|
-
this._overlayElement.restoreFocusOnClose = true;
|
|
3438
|
-
} else {
|
|
3439
|
-
this._onClosed();
|
|
3440
|
-
if (this._openedWithFocusRing && this._isInputFocused()) {
|
|
3441
|
-
this.setAttribute('focus-ring', '');
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
|
-
|
|
3445
|
-
const input = this._nativeInput;
|
|
3446
|
-
if (input) {
|
|
3447
|
-
input.setAttribute('aria-expanded', !!opened);
|
|
3448
|
-
|
|
3449
|
-
if (opened) {
|
|
3450
|
-
input.setAttribute('aria-controls', this._scroller.id);
|
|
3451
|
-
} else {
|
|
3452
|
-
input.removeAttribute('aria-controls');
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
|
|
3457
|
-
/** @private */
|
|
3458
|
-
_onOverlayTouchAction() {
|
|
3459
|
-
// On touch devices, blur the input on touch start inside the overlay, in order to hide
|
|
3460
|
-
// the virtual keyboard. But don't close the overlay on this blur.
|
|
3461
|
-
this._closeOnBlurIsPrevented = true;
|
|
3462
|
-
this.inputElement.blur();
|
|
3463
|
-
this._closeOnBlurIsPrevented = false;
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
|
-
/** @protected */
|
|
3467
|
-
_isClearButton(event) {
|
|
3468
|
-
return event.composedPath()[0] === this.clearElement;
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
|
-
/** @private */
|
|
3472
|
-
__onClearButtonMouseDown(event) {
|
|
3473
|
-
event.preventDefault(); // Prevent native focusout event
|
|
3474
|
-
this.inputElement.focus();
|
|
3475
|
-
}
|
|
3476
|
-
|
|
3477
|
-
/**
|
|
3478
|
-
* @param {Event} event
|
|
3479
|
-
* @protected
|
|
3480
|
-
*/
|
|
3481
|
-
_onClearButtonClick(event) {
|
|
3482
|
-
event.preventDefault();
|
|
3483
|
-
this._onClearAction();
|
|
3484
|
-
|
|
3485
|
-
// De-select dropdown item
|
|
3486
|
-
if (this.opened) {
|
|
3487
|
-
this.requestContentUpdate();
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
/**
|
|
3492
|
-
* @param {Event} event
|
|
3493
|
-
* @private
|
|
3494
|
-
*/
|
|
3495
|
-
_onToggleButtonClick(event) {
|
|
3496
|
-
// Prevent parent components such as `vaadin-grid`
|
|
3497
|
-
// from handling the click event after it bubbles.
|
|
3498
|
-
event.preventDefault();
|
|
3499
|
-
|
|
3500
|
-
if (this.opened) {
|
|
3501
|
-
this.close();
|
|
3502
|
-
} else {
|
|
3503
|
-
this.open();
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
/**
|
|
3508
|
-
* @param {Event} event
|
|
3509
|
-
* @protected
|
|
3510
|
-
*/
|
|
3511
|
-
_onHostClick(event) {
|
|
3512
|
-
if (!this.autoOpenDisabled) {
|
|
3513
|
-
event.preventDefault();
|
|
3514
|
-
this.open();
|
|
3515
|
-
}
|
|
3516
|
-
}
|
|
3517
|
-
|
|
3518
|
-
/** @private */
|
|
3519
|
-
_onClick(event) {
|
|
3520
|
-
if (this._isClearButton(event)) {
|
|
3521
|
-
this._onClearButtonClick(event);
|
|
3522
|
-
} else if (event.composedPath().includes(this._toggleElement)) {
|
|
3523
|
-
this._onToggleButtonClick(event);
|
|
3524
|
-
} else {
|
|
3525
|
-
this._onHostClick(event);
|
|
3526
|
-
}
|
|
3527
|
-
}
|
|
3528
|
-
|
|
3529
|
-
/**
|
|
3530
|
-
* Override an event listener from `KeyboardMixin`.
|
|
3531
|
-
*
|
|
3532
|
-
* @param {KeyboardEvent} e
|
|
3533
|
-
* @protected
|
|
3534
|
-
* @override
|
|
3535
|
-
*/
|
|
3536
|
-
_onKeyDown(e) {
|
|
3537
|
-
super._onKeyDown(e);
|
|
3538
|
-
|
|
3539
|
-
if (e.key === 'Tab') {
|
|
3540
|
-
this._overlayElement.restoreFocusOnClose = false;
|
|
3541
|
-
} else if (e.key === 'ArrowDown') {
|
|
3542
|
-
this._onArrowDown();
|
|
3543
|
-
|
|
3544
|
-
// Prevent caret from moving
|
|
3545
|
-
e.preventDefault();
|
|
3546
|
-
} else if (e.key === 'ArrowUp') {
|
|
3547
|
-
this._onArrowUp();
|
|
3548
|
-
|
|
3549
|
-
// Prevent caret from moving
|
|
3550
|
-
e.preventDefault();
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
/** @private */
|
|
3555
|
-
_getItemLabel(item) {
|
|
3556
|
-
let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
|
|
3557
|
-
if (label === undefined || label === null) {
|
|
3558
|
-
label = item ? item.toString() : '';
|
|
3559
|
-
}
|
|
3560
|
-
return label;
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
/** @private */
|
|
3564
|
-
_getItemValue(item) {
|
|
3565
|
-
let value = item && this.itemValuePath ? get(this.itemValuePath, item) : undefined;
|
|
3566
|
-
if (value === undefined) {
|
|
3567
|
-
value = item ? item.toString() : '';
|
|
3568
|
-
}
|
|
3569
|
-
return value;
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
/** @private */
|
|
3573
|
-
_onArrowDown() {
|
|
3574
|
-
if (this.opened) {
|
|
3575
|
-
const items = this._dropdownItems;
|
|
3576
|
-
if (items) {
|
|
3577
|
-
this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
|
|
3578
|
-
this._prefillFocusedItemLabel();
|
|
3579
|
-
}
|
|
3580
|
-
} else {
|
|
3581
|
-
this.open();
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
|
|
3585
|
-
/** @private */
|
|
3586
|
-
_onArrowUp() {
|
|
3587
|
-
if (this.opened) {
|
|
3588
|
-
if (this._focusedIndex > -1) {
|
|
3589
|
-
this._focusedIndex = Math.max(0, this._focusedIndex - 1);
|
|
3590
|
-
} else {
|
|
3591
|
-
const items = this._dropdownItems;
|
|
3592
|
-
if (items) {
|
|
3593
|
-
this._focusedIndex = items.length - 1;
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
|
-
this._prefillFocusedItemLabel();
|
|
3598
|
-
} else {
|
|
3599
|
-
this.open();
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3602
|
-
|
|
3603
|
-
/** @private */
|
|
3604
|
-
_prefillFocusedItemLabel() {
|
|
3605
|
-
if (this._focusedIndex > -1) {
|
|
3606
|
-
const focusedItem = this._dropdownItems[this._focusedIndex];
|
|
3607
|
-
this._inputElementValue = this._getItemLabel(focusedItem);
|
|
3608
|
-
this._markAllSelectionRange();
|
|
3609
|
-
}
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
/** @private */
|
|
3613
|
-
_setSelectionRange(start, end) {
|
|
3614
|
-
// Setting selection range focuses and/or moves the caret in some browsers,
|
|
3615
|
-
// and there's no need to modify the selection range if the input isn't focused anyway.
|
|
3616
|
-
// This affects Safari. When the overlay is open, and then hitting tab, browser should focus
|
|
3617
|
-
// the next focusable element instead of the combo-box itself.
|
|
3618
|
-
if (this._isInputFocused() && this.inputElement.setSelectionRange) {
|
|
3619
|
-
this.inputElement.setSelectionRange(start, end);
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
/** @private */
|
|
3624
|
-
_markAllSelectionRange() {
|
|
3625
|
-
if (this._inputElementValue !== undefined) {
|
|
3626
|
-
this._setSelectionRange(0, this._inputElementValue.length);
|
|
3627
|
-
}
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
/** @private */
|
|
3631
|
-
_clearSelectionRange() {
|
|
3632
|
-
if (this._inputElementValue !== undefined) {
|
|
3633
|
-
const pos = this._inputElementValue ? this._inputElementValue.length : 0;
|
|
3634
|
-
this._setSelectionRange(pos, pos);
|
|
3635
|
-
}
|
|
3636
|
-
}
|
|
3637
|
-
|
|
3638
|
-
/** @private */
|
|
3639
|
-
_closeOrCommit() {
|
|
3640
|
-
if (!this.opened && !this.loading) {
|
|
3641
|
-
this._commitValue();
|
|
3642
|
-
} else {
|
|
3643
|
-
this.close();
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3646
|
-
|
|
3647
|
-
/**
|
|
3648
|
-
* Override an event listener from `KeyboardMixin`.
|
|
3649
|
-
*
|
|
3650
|
-
* @param {KeyboardEvent} e
|
|
3651
|
-
* @protected
|
|
3652
|
-
* @override
|
|
3653
|
-
*/
|
|
3654
|
-
_onEnter(e) {
|
|
3655
|
-
// Do not commit value when custom values are disallowed and input value is not a valid option
|
|
3656
|
-
// also stop propagation of the event, otherwise the user could submit a form while the input
|
|
3657
|
-
// still contains an invalid value
|
|
3658
|
-
const hasInvalidOption =
|
|
3659
|
-
this._focusedIndex < 0 &&
|
|
3660
|
-
this._inputElementValue !== '' &&
|
|
3661
|
-
this._getItemLabel(this.selectedItem) !== this._inputElementValue;
|
|
3662
|
-
if (!this.allowCustomValue && hasInvalidOption) {
|
|
3663
|
-
// Do not submit the surrounding form.
|
|
3664
|
-
e.preventDefault();
|
|
3665
|
-
// Do not trigger global listeners
|
|
3666
|
-
e.stopPropagation();
|
|
3667
|
-
return;
|
|
3668
|
-
}
|
|
3669
|
-
|
|
3670
|
-
// Stop propagation of the enter event only if the dropdown is opened, this
|
|
3671
|
-
// "consumes" the enter event for the action of closing the dropdown
|
|
3672
|
-
if (this.opened) {
|
|
3673
|
-
// Do not submit the surrounding form.
|
|
3674
|
-
e.preventDefault();
|
|
3675
|
-
// Do not trigger global listeners
|
|
3676
|
-
e.stopPropagation();
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
|
-
this._closeOrCommit();
|
|
3680
|
-
}
|
|
3681
|
-
|
|
3682
|
-
/**
|
|
3683
|
-
* Override an event listener from `KeyboardMixin`.
|
|
3684
|
-
* Do not call `super` in order to override clear
|
|
3685
|
-
* button logic defined in `InputControlMixin`.
|
|
3686
|
-
*
|
|
3687
|
-
* @param {!KeyboardEvent} e
|
|
3688
|
-
* @protected
|
|
3689
|
-
* @override
|
|
3690
|
-
*/
|
|
3691
|
-
_onEscape(e) {
|
|
3692
|
-
if (this.autoOpenDisabled) {
|
|
3693
|
-
// Auto-open is disabled
|
|
3694
|
-
if (this.opened || (this.value !== this._inputElementValue && this._inputElementValue.length > 0)) {
|
|
3695
|
-
// The overlay is open or
|
|
3696
|
-
// The input value has changed but the change hasn't been committed, so cancel it.
|
|
3697
|
-
e.stopPropagation();
|
|
3698
|
-
this._focusedIndex = -1;
|
|
3699
|
-
this.cancel();
|
|
3700
|
-
} else if (this.clearButtonVisible && !this.opened && !!this.value) {
|
|
3701
|
-
e.stopPropagation();
|
|
3702
|
-
// The clear button is visible and the overlay is closed, so clear the value.
|
|
3703
|
-
this._onClearAction();
|
|
3704
|
-
}
|
|
3705
|
-
} else if (this.opened) {
|
|
3706
|
-
// Auto-open is enabled
|
|
3707
|
-
// The overlay is open
|
|
3708
|
-
e.stopPropagation();
|
|
3709
|
-
|
|
3710
|
-
if (this._focusedIndex > -1) {
|
|
3711
|
-
// An item is focused, revert the input to the filtered value
|
|
3712
|
-
this._focusedIndex = -1;
|
|
3713
|
-
this._revertInputValue();
|
|
3714
|
-
} else {
|
|
3715
|
-
// No item is focused, cancel the change and close the overlay
|
|
3716
|
-
this.cancel();
|
|
3717
|
-
}
|
|
3718
|
-
} else if (this.clearButtonVisible && !!this.value) {
|
|
3719
|
-
e.stopPropagation();
|
|
3720
|
-
// The clear button is visible and the overlay is closed, so clear the value.
|
|
3721
|
-
this._onClearAction();
|
|
3722
|
-
}
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
|
-
/** @private */
|
|
3726
|
-
_toggleElementChanged(toggleElement) {
|
|
3727
|
-
if (toggleElement) {
|
|
3728
|
-
// Don't blur the input on toggle mousedown
|
|
3729
|
-
toggleElement.addEventListener('mousedown', (e) => e.preventDefault());
|
|
3730
|
-
// Unfocus previously focused element if focus is not inside combo box (on touch devices)
|
|
3731
|
-
toggleElement.addEventListener('click', () => {
|
|
3732
|
-
if (isTouch && !this._isInputFocused()) {
|
|
3733
|
-
document.activeElement.blur();
|
|
3734
|
-
}
|
|
3735
|
-
});
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
|
|
3739
|
-
/**
|
|
3740
|
-
* Clears the current value.
|
|
3741
|
-
* @protected
|
|
3742
|
-
*/
|
|
3743
|
-
_onClearAction() {
|
|
3744
|
-
this.selectedItem = null;
|
|
3745
|
-
|
|
3746
|
-
if (this.allowCustomValue) {
|
|
3747
|
-
this.value = '';
|
|
3748
|
-
}
|
|
3749
|
-
|
|
3750
|
-
this._detectAndDispatchChange();
|
|
3751
|
-
}
|
|
3752
|
-
|
|
3753
|
-
/**
|
|
3754
|
-
* Reverts back to original value.
|
|
3755
|
-
*/
|
|
3756
|
-
cancel() {
|
|
3757
|
-
this._revertInputValueToValue();
|
|
3758
|
-
// In the next _detectAndDispatchChange() call, the change detection should not pass
|
|
3759
|
-
this._lastCommittedValue = this.value;
|
|
3760
|
-
this._closeOrCommit();
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
/** @private */
|
|
3764
|
-
_onOpened() {
|
|
3765
|
-
// _detectAndDispatchChange() should not consider value changes done before opening
|
|
3766
|
-
this._lastCommittedValue = this.value;
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
/** @private */
|
|
3770
|
-
_onClosed() {
|
|
3771
|
-
if (!this.loading || this.allowCustomValue) {
|
|
3772
|
-
this._commitValue();
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
|
|
3776
|
-
/** @private */
|
|
3777
|
-
_commitValue() {
|
|
3778
|
-
if (this._focusedIndex > -1) {
|
|
3779
|
-
const focusedItem = this._dropdownItems[this._focusedIndex];
|
|
3780
|
-
if (this.selectedItem !== focusedItem) {
|
|
3781
|
-
this.selectedItem = focusedItem;
|
|
3782
|
-
}
|
|
3783
|
-
// Make sure input field is updated in case value doesn't change (i.e. FOO -> foo)
|
|
3784
|
-
this._inputElementValue = this._getItemLabel(this.selectedItem);
|
|
3785
|
-
this._focusedIndex = -1;
|
|
3786
|
-
} else if (this._inputElementValue === '' || this._inputElementValue === undefined) {
|
|
3787
|
-
this.selectedItem = null;
|
|
3788
|
-
|
|
3789
|
-
if (this.allowCustomValue) {
|
|
3790
|
-
this.value = '';
|
|
3791
|
-
}
|
|
3792
|
-
} else {
|
|
3793
|
-
// Try to find an item which label matches the input value.
|
|
3794
|
-
const items = [this.selectedItem, ...(this._dropdownItems || [])];
|
|
3795
|
-
const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
|
|
3796
|
-
|
|
3797
|
-
if (
|
|
3798
|
-
this.allowCustomValue &&
|
|
3799
|
-
// To prevent a repetitive input value being saved after pressing ESC and Tab.
|
|
3800
|
-
!itemMatchingInputValue
|
|
3801
|
-
) {
|
|
3802
|
-
const customValue = this._inputElementValue;
|
|
3803
|
-
|
|
3804
|
-
// Store reference to the last custom value for checking it on focusout.
|
|
3805
|
-
this._lastCustomValue = customValue;
|
|
3806
|
-
|
|
3807
|
-
// An item matching by label was not found, but custom values are allowed.
|
|
3808
|
-
// Dispatch a custom-value-set event with the input value.
|
|
3809
|
-
const e = new CustomEvent('custom-value-set', {
|
|
3810
|
-
detail: customValue,
|
|
3811
|
-
composed: true,
|
|
3812
|
-
cancelable: true,
|
|
3813
|
-
bubbles: true,
|
|
3814
|
-
});
|
|
3815
|
-
this.dispatchEvent(e);
|
|
3816
|
-
if (!e.defaultPrevented) {
|
|
3817
|
-
this.value = customValue;
|
|
3818
|
-
}
|
|
3819
|
-
} else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
|
|
3820
|
-
// An item matching by label was found, select it.
|
|
3821
|
-
this.value = this._getItemValue(itemMatchingInputValue);
|
|
3822
|
-
} else {
|
|
3823
|
-
// Revert the input value
|
|
3824
|
-
this._inputElementValue = this.selectedItem ? this._getItemLabel(this.selectedItem) : this.value || '';
|
|
3825
|
-
}
|
|
3826
|
-
}
|
|
3827
|
-
|
|
3828
|
-
this._detectAndDispatchChange();
|
|
3829
|
-
|
|
3830
|
-
this._clearSelectionRange();
|
|
3831
|
-
|
|
3832
|
-
this.filter = '';
|
|
3833
|
-
}
|
|
3834
|
-
|
|
3835
|
-
/**
|
|
3836
|
-
* Override an event listener from `InputMixin`.
|
|
3837
|
-
* @param {!Event} event
|
|
3838
|
-
* @protected
|
|
3839
|
-
* @override
|
|
3840
|
-
*/
|
|
3841
|
-
_onInput(event) {
|
|
3842
|
-
const filter = this._inputElementValue;
|
|
3843
|
-
|
|
3844
|
-
// When opening dropdown on user input, both `opened` and `filter` properties are set.
|
|
3845
|
-
// Perform a batched property update instead of relying on sync property observers.
|
|
3846
|
-
// This is necessary to avoid an extra data-provider request for loading first page.
|
|
3847
|
-
const props = {};
|
|
3848
|
-
|
|
3849
|
-
if (this.filter === filter) {
|
|
3850
|
-
// Filter and input value might get out of sync, while keyboard navigating for example.
|
|
3851
|
-
// Afterwards, input value might be changed to the same value as used in filtering.
|
|
3852
|
-
// In situation like these, we need to make sure all the filter changes handlers are run.
|
|
3853
|
-
this._filterChanged(this.filter);
|
|
3854
|
-
} else {
|
|
3855
|
-
props.filter = filter;
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
|
-
if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
|
|
3859
|
-
props.opened = true;
|
|
3860
|
-
}
|
|
3861
|
-
|
|
3862
|
-
this.setProperties(props);
|
|
3863
|
-
}
|
|
3864
|
-
|
|
3865
|
-
/**
|
|
3866
|
-
* Override an event listener from `InputMixin`.
|
|
3867
|
-
* @param {!Event} event
|
|
3868
|
-
* @protected
|
|
3869
|
-
* @override
|
|
3870
|
-
*/
|
|
3871
|
-
_onChange(event) {
|
|
3872
|
-
// Suppress the native change event fired on the native input.
|
|
3873
|
-
// We use `_detectAndDispatchChange` to fire a custom event.
|
|
3874
|
-
event.stopPropagation();
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
/** @private */
|
|
3878
|
-
_itemLabelPathChanged(itemLabelPath) {
|
|
3879
|
-
if (typeof itemLabelPath !== 'string') {
|
|
3880
|
-
console.error('You should set itemLabelPath to a valid string');
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
|
|
3884
|
-
/** @private */
|
|
3885
|
-
_filterChanged(filter) {
|
|
3886
|
-
// Scroll to the top of the list whenever the filter changes.
|
|
3887
|
-
this._scrollIntoView(0);
|
|
3888
|
-
|
|
3889
|
-
this._focusedIndex = -1;
|
|
3890
|
-
|
|
3891
|
-
if (this.items) {
|
|
3892
|
-
this.filteredItems = this._filterItems(this.items, filter);
|
|
3893
|
-
} else {
|
|
3894
|
-
// With certain use cases (e. g., external filtering), `items` are
|
|
3895
|
-
// undefined. Filtering is unnecessary per se, but the filteredItems
|
|
3896
|
-
// observer should still be invoked to update focused item.
|
|
3897
|
-
this._filteredItemsChanged(this.filteredItems);
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
|
-
|
|
3901
|
-
/** @protected */
|
|
3902
|
-
_revertInputValue() {
|
|
3903
|
-
if (this.filter !== '') {
|
|
3904
|
-
this._inputElementValue = this.filter;
|
|
3905
|
-
} else {
|
|
3906
|
-
this._revertInputValueToValue();
|
|
3907
|
-
}
|
|
3908
|
-
this._clearSelectionRange();
|
|
3909
|
-
}
|
|
3910
|
-
|
|
3911
|
-
/** @private */
|
|
3912
|
-
_revertInputValueToValue() {
|
|
3913
|
-
if (this.allowCustomValue && !this.selectedItem) {
|
|
3914
|
-
this._inputElementValue = this.value;
|
|
3915
|
-
} else {
|
|
3916
|
-
this._inputElementValue = this._getItemLabel(this.selectedItem);
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
|
-
/** @private */
|
|
3921
|
-
_selectedItemChanged(selectedItem) {
|
|
3922
|
-
if (selectedItem === null || selectedItem === undefined) {
|
|
3923
|
-
if (this.filteredItems) {
|
|
3924
|
-
if (!this.allowCustomValue) {
|
|
3925
|
-
this.value = '';
|
|
3926
|
-
}
|
|
3927
|
-
|
|
3928
|
-
this._toggleHasValue(this._hasValue);
|
|
3929
|
-
this._inputElementValue = this.value;
|
|
3930
|
-
}
|
|
3931
|
-
} else {
|
|
3932
|
-
const value = this._getItemValue(selectedItem);
|
|
3933
|
-
if (this.value !== value) {
|
|
3934
|
-
this.value = value;
|
|
3935
|
-
if (this.value !== value) {
|
|
3936
|
-
// The value was changed to something else in value-changed listener,
|
|
3937
|
-
// so prevent from resetting it to the previous value.
|
|
3938
|
-
return;
|
|
3939
|
-
}
|
|
3940
|
-
}
|
|
3941
|
-
|
|
3942
|
-
this._toggleHasValue(true);
|
|
3943
|
-
this._inputElementValue = this._getItemLabel(selectedItem);
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
|
-
/**
|
|
3948
|
-
* Override an observer from `InputMixin`.
|
|
3949
|
-
* @protected
|
|
3950
|
-
* @override
|
|
3951
|
-
*/
|
|
3952
|
-
_valueChanged(value, oldVal) {
|
|
3953
|
-
if (value === '' && oldVal === undefined) {
|
|
3954
|
-
// Initializing, no need to do anything
|
|
3955
|
-
// See https://github.com/vaadin/vaadin-combo-box/issues/554
|
|
3956
|
-
return;
|
|
3957
|
-
}
|
|
3958
|
-
|
|
3959
|
-
if (isValidValue(value)) {
|
|
3960
|
-
if (this._getItemValue(this.selectedItem) !== value) {
|
|
3961
|
-
this._selectItemForValue(value);
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
|
-
if (!this.selectedItem && this.allowCustomValue) {
|
|
3965
|
-
this._inputElementValue = value;
|
|
3966
|
-
}
|
|
3967
|
-
|
|
3968
|
-
this._toggleHasValue(this._hasValue);
|
|
3969
|
-
} else {
|
|
3970
|
-
this.selectedItem = null;
|
|
3971
|
-
}
|
|
3972
|
-
|
|
3973
|
-
this.filter = '';
|
|
3974
|
-
|
|
3975
|
-
// In the next _detectAndDispatchChange() call, the change detection should pass
|
|
3976
|
-
this._lastCommittedValue = undefined;
|
|
3977
|
-
}
|
|
3978
|
-
|
|
3979
|
-
/** @private */
|
|
3980
|
-
_detectAndDispatchChange() {
|
|
3981
|
-
// Do not validate when focusout is caused by document
|
|
3982
|
-
// losing focus, which happens on browser tab switch.
|
|
3983
|
-
if (document.hasFocus()) {
|
|
3984
|
-
this.validate();
|
|
3985
|
-
}
|
|
3986
|
-
|
|
3987
|
-
if (this.value !== this._lastCommittedValue) {
|
|
3988
|
-
this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
|
3989
|
-
this._lastCommittedValue = this.value;
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
|
|
3993
|
-
/** @private */
|
|
3994
|
-
_itemsChanged(items, oldItems) {
|
|
3995
|
-
this._ensureItemsOrDataProvider(() => {
|
|
3996
|
-
this.items = oldItems;
|
|
3997
|
-
});
|
|
3998
|
-
|
|
3999
|
-
if (items) {
|
|
4000
|
-
this.filteredItems = items.slice(0);
|
|
4001
|
-
} else if (oldItems) {
|
|
4002
|
-
// Only clear filteredItems if the component had items previously but got cleared
|
|
4003
|
-
this.filteredItems = null;
|
|
4004
|
-
}
|
|
4005
|
-
}
|
|
4006
|
-
|
|
4007
|
-
/** @private */
|
|
4008
|
-
_filteredItemsChanged(filteredItems, oldFilteredItems) {
|
|
4009
|
-
this._setDropdownItems(filteredItems);
|
|
4010
|
-
|
|
4011
|
-
// Store the currently focused item if any. The focused index preserves
|
|
4012
|
-
// in the case when more filtered items are loading but it is reset
|
|
4013
|
-
// when the user types in a filter query.
|
|
4014
|
-
const focusedItem = oldFilteredItems ? oldFilteredItems[this._focusedIndex] : null;
|
|
4015
|
-
|
|
4016
|
-
// Try to sync `selectedItem` based on `value` once a new set of `filteredItems` is available
|
|
4017
|
-
// (as a result of external filtering or when they have been loaded by the data provider).
|
|
4018
|
-
// When `value` is specified but `selectedItem` is not, it means that there was no item
|
|
4019
|
-
// matching `value` at the moment `value` was set, so `selectedItem` has remained unsynced.
|
|
4020
|
-
const valueIndex = this.__getItemIndexByValue(filteredItems, this.value);
|
|
4021
|
-
if ((this.selectedItem === null || this.selectedItem === undefined) && valueIndex >= 0) {
|
|
4022
|
-
this.selectedItem = filteredItems[valueIndex];
|
|
4023
|
-
}
|
|
4024
|
-
|
|
4025
|
-
// Try to first set focus on the item that had been focused before `filteredItems` were updated
|
|
4026
|
-
// if it is still present in the `filteredItems` array. Otherwise, set the focused index
|
|
4027
|
-
// depending on the selected item or the filter query.
|
|
4028
|
-
const focusedItemIndex = this.__getItemIndexByValue(filteredItems, this._getItemValue(focusedItem));
|
|
4029
|
-
if (focusedItemIndex > -1) {
|
|
4030
|
-
this._focusedIndex = focusedItemIndex;
|
|
4031
|
-
} else {
|
|
4032
|
-
// When the user filled in something that is different from the current value = filtering is enabled,
|
|
4033
|
-
// set the focused index to the item that matches the filter query.
|
|
4034
|
-
this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this.filter);
|
|
4035
|
-
}
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4038
|
-
/** @private */
|
|
4039
|
-
_filterItems(arr, filter) {
|
|
4040
|
-
if (!arr) {
|
|
4041
|
-
return arr;
|
|
4042
|
-
}
|
|
4043
|
-
|
|
4044
|
-
const filteredItems = arr.filter((item) => {
|
|
4045
|
-
filter = filter ? filter.toString().toLowerCase() : '';
|
|
4046
|
-
// Check if item contains input value.
|
|
4047
|
-
return this._getItemLabel(item).toString().toLowerCase().indexOf(filter) > -1;
|
|
4048
|
-
});
|
|
4049
|
-
|
|
4050
|
-
return filteredItems;
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
/** @private */
|
|
4054
|
-
_selectItemForValue(value) {
|
|
4055
|
-
const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
|
|
4056
|
-
const previouslySelectedItem = this.selectedItem;
|
|
4057
|
-
|
|
4058
|
-
if (valueIndex >= 0) {
|
|
4059
|
-
this.selectedItem = this.filteredItems[valueIndex];
|
|
4060
|
-
} else if (this.dataProvider && this.selectedItem === undefined) {
|
|
4061
|
-
this.selectedItem = undefined;
|
|
4062
|
-
} else {
|
|
4063
|
-
this.selectedItem = null;
|
|
4064
|
-
}
|
|
4065
|
-
|
|
4066
|
-
if (this.selectedItem === null && previouslySelectedItem === null) {
|
|
4067
|
-
this._selectedItemChanged(this.selectedItem);
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
|
|
4071
|
-
/**
|
|
4072
|
-
* Provide items to be rendered in the dropdown.
|
|
4073
|
-
* Override this method to show custom items.
|
|
4074
|
-
*
|
|
4075
|
-
* @protected
|
|
4076
|
-
*/
|
|
4077
|
-
_setDropdownItems(items) {
|
|
4078
|
-
this._dropdownItems = items;
|
|
4079
|
-
}
|
|
4080
|
-
|
|
4081
|
-
/** @private */
|
|
4082
|
-
_getItemElements() {
|
|
4083
|
-
return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
/** @private */
|
|
4087
|
-
_scrollIntoView(index) {
|
|
4088
|
-
if (!this._scroller) {
|
|
4089
|
-
return;
|
|
4090
|
-
}
|
|
4091
|
-
this._scroller.scrollIntoView(index);
|
|
4092
|
-
}
|
|
4093
|
-
|
|
4094
|
-
/**
|
|
4095
|
-
* Returns the first item that matches the provided value.
|
|
4096
|
-
*
|
|
4097
|
-
* @private
|
|
4098
|
-
*/
|
|
4099
|
-
__getItemIndexByValue(items, value) {
|
|
4100
|
-
if (!items || !isValidValue(value)) {
|
|
4101
|
-
return -1;
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
return findItemIndex(items, (item) => {
|
|
4105
|
-
return this._getItemValue(item) === value;
|
|
4106
|
-
});
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
|
-
/**
|
|
4110
|
-
* Returns the first item that matches the provided label.
|
|
4111
|
-
* Labels are matched against each other case insensitively.
|
|
4112
|
-
*
|
|
4113
|
-
* @private
|
|
4114
|
-
*/
|
|
4115
|
-
__getItemIndexByLabel(items, label) {
|
|
4116
|
-
if (!items || !label) {
|
|
4117
|
-
return -1;
|
|
4118
|
-
}
|
|
4119
|
-
|
|
4120
|
-
return findItemIndex(items, (item) => {
|
|
4121
|
-
return this._getItemLabel(item).toString().toLowerCase() === label.toString().toLowerCase();
|
|
4122
|
-
});
|
|
4123
|
-
}
|
|
4124
|
-
|
|
4125
|
-
/** @private */
|
|
4126
|
-
_overlaySelectedItemChanged(e) {
|
|
4127
|
-
// Stop this private event from leaking outside.
|
|
4128
|
-
e.stopPropagation();
|
|
4129
|
-
|
|
4130
|
-
if (e.detail.item instanceof ComboBoxPlaceholder) {
|
|
4131
|
-
// Placeholder items should not be selectable.
|
|
4132
|
-
return;
|
|
4133
|
-
}
|
|
4134
|
-
|
|
4135
|
-
if (this.opened) {
|
|
4136
|
-
this._focusedIndex = this.filteredItems.indexOf(e.detail.item);
|
|
4137
|
-
this.close();
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
|
|
4141
|
-
/**
|
|
4142
|
-
* Override method inherited from `FocusMixin`
|
|
4143
|
-
* to close the overlay on blur and commit the value.
|
|
4144
|
-
*
|
|
4145
|
-
* @param {boolean} focused
|
|
4146
|
-
* @protected
|
|
4147
|
-
* @override
|
|
4148
|
-
*/
|
|
4149
|
-
_setFocused(focused) {
|
|
4150
|
-
super._setFocused(focused);
|
|
4151
|
-
|
|
4152
|
-
if (!focused && !this.readonly && !this._closeOnBlurIsPrevented) {
|
|
4153
|
-
// User's logic in `custom-value-set` event listener might cause input to blur,
|
|
4154
|
-
// which will result in attempting to commit the same custom value once again.
|
|
4155
|
-
if (!this.opened && this.allowCustomValue && this._inputElementValue === this._lastCustomValue) {
|
|
4156
|
-
delete this._lastCustomValue;
|
|
4157
|
-
return;
|
|
4158
|
-
}
|
|
4159
|
-
|
|
4160
|
-
this._closeOrCommit();
|
|
4161
|
-
}
|
|
4162
|
-
}
|
|
4163
|
-
|
|
4164
|
-
/**
|
|
4165
|
-
* Override method inherited from `FocusMixin` to not remove focused
|
|
4166
|
-
* state when focus moves to the overlay.
|
|
4167
|
-
*
|
|
4168
|
-
* @param {FocusEvent} event
|
|
4169
|
-
* @return {boolean}
|
|
4170
|
-
* @protected
|
|
4171
|
-
* @override
|
|
4172
|
-
*/
|
|
4173
|
-
_shouldRemoveFocus(event) {
|
|
4174
|
-
// VoiceOver on iOS fires `focusout` event when moving focus to the item in the dropdown.
|
|
4175
|
-
// Do not focus the input in this case, because it would break announcement for the item.
|
|
4176
|
-
if (event.relatedTarget && event.relatedTarget.localName === `${this._tagNamePrefix}-item`) {
|
|
4177
|
-
return false;
|
|
4178
|
-
}
|
|
4179
|
-
|
|
4180
|
-
// Do not blur when focus moves to the overlay
|
|
4181
|
-
// Also, fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
|
|
4182
|
-
if (event.relatedTarget === this._overlayElement) {
|
|
4183
|
-
event.composedPath()[0].focus();
|
|
4184
|
-
return false;
|
|
4185
|
-
}
|
|
4186
|
-
|
|
4187
|
-
return true;
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
|
-
/** @private */
|
|
4191
|
-
_onTouchend(event) {
|
|
4192
|
-
if (!this.clearElement || event.composedPath()[0] !== this.clearElement) {
|
|
4193
|
-
return;
|
|
4194
|
-
}
|
|
4195
|
-
|
|
4196
|
-
event.preventDefault();
|
|
4197
|
-
this._onClearAction();
|
|
4198
|
-
}
|
|
4199
|
-
|
|
4200
|
-
/**
|
|
4201
|
-
* Fired when the value changes.
|
|
4202
|
-
*
|
|
4203
|
-
* @event value-changed
|
|
4204
|
-
* @param {Object} detail
|
|
4205
|
-
* @param {String} detail.value the combobox value
|
|
4206
|
-
*/
|
|
4207
|
-
|
|
4208
|
-
/**
|
|
4209
|
-
* Fired when selected item changes.
|
|
4210
|
-
*
|
|
4211
|
-
* @event selected-item-changed
|
|
4212
|
-
* @param {Object} detail
|
|
4213
|
-
* @param {Object|String} detail.value the selected item. Type is the same as the type of `items`.
|
|
4214
|
-
*/
|
|
4215
|
-
|
|
4216
|
-
/**
|
|
4217
|
-
* Fired when the user sets a custom value.
|
|
4218
|
-
* @event custom-value-set
|
|
4219
|
-
* @param {String} detail the custom value
|
|
4220
|
-
*/
|
|
4221
|
-
|
|
4222
|
-
/**
|
|
4223
|
-
* Fired when value changes.
|
|
4224
|
-
* To comply with https://developer.mozilla.org/en-US/docs/Web/Events/change
|
|
4225
|
-
* @event change
|
|
4226
|
-
*/
|
|
4227
|
-
|
|
4228
|
-
/**
|
|
4229
|
-
* Fired after the `vaadin-combo-box-overlay` opens.
|
|
4230
|
-
*
|
|
4231
|
-
* @event vaadin-combo-box-dropdown-opened
|
|
4232
|
-
*/
|
|
4233
|
-
|
|
4234
|
-
/**
|
|
4235
|
-
* Fired after the `vaadin-combo-box-overlay` closes.
|
|
4236
|
-
*
|
|
4237
|
-
* @event vaadin-combo-box-dropdown-closed
|
|
4238
|
-
*/
|
|
4239
|
-
};
|
|
4240
|
-
|
|
4241
|
-
/**
|
|
4242
|
-
* @license
|
|
4243
|
-
* Copyright (c) 2015 - 2023 Vaadin Ltd.
|
|
4244
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
4245
|
-
*/
|
|
4246
|
-
|
|
4247
|
-
registerStyles('vaadin-combo-box', inputFieldShared$1, { moduleId: 'vaadin-combo-box-styles' });
|
|
4248
|
-
|
|
4249
|
-
/**
|
|
4250
|
-
* `<vaadin-combo-box>` is a web component for choosing a value from a filterable list of options
|
|
4251
|
-
* presented in a dropdown overlay. The options can be provided as a list of strings or objects
|
|
4252
|
-
* by setting [`items`](#/elements/vaadin-combo-box#property-items) property on the element.
|
|
4253
|
-
*
|
|
4254
|
-
* ```html
|
|
4255
|
-
* <vaadin-combo-box id="combo-box"></vaadin-combo-box>
|
|
4256
|
-
* ```
|
|
4257
|
-
*
|
|
4258
|
-
* ```js
|
|
4259
|
-
* document.querySelector('#combo-box').items = ['apple', 'orange', 'banana'];
|
|
4260
|
-
* ```
|
|
4261
|
-
*
|
|
4262
|
-
* When the selected `value` is changed, a `value-changed` event is triggered.
|
|
4263
|
-
*
|
|
4264
|
-
* ### Item rendering
|
|
4265
|
-
*
|
|
4266
|
-
* To customize the content of the `<vaadin-combo-box-item>` elements placed in the dropdown, use
|
|
4267
|
-
* [`renderer`](#/elements/vaadin-combo-box#property-renderer) property which accepts a function.
|
|
4268
|
-
* The renderer function is called with `root`, `comboBox`, and `model` as arguments.
|
|
4269
|
-
*
|
|
4270
|
-
* Generate DOM content by using `model` object properties if needed, and append it to the `root`
|
|
4271
|
-
* element. The `comboBox` reference is provided to access the combo-box element state. Do not
|
|
4272
|
-
* set combo-box properties in a `renderer` function.
|
|
4273
|
-
*
|
|
4274
|
-
* ```js
|
|
4275
|
-
* const comboBox = document.querySelector('#combo-box');
|
|
4276
|
-
* comboBox.items = [{'label': 'Hydrogen', 'value': 'H'}];
|
|
4277
|
-
* comboBox.renderer = (root, comboBox, model) => {
|
|
4278
|
-
* const item = model.item;
|
|
4279
|
-
* root.innerHTML = `${model.index}: ${item.label} <b>${item.value}</b>`;
|
|
4280
|
-
* };
|
|
4281
|
-
* ```
|
|
4282
|
-
*
|
|
4283
|
-
* Renderer is called on the opening of the combo-box and each time the related model is updated.
|
|
4284
|
-
* Before creating new content, it is recommended to check if there is already an existing DOM
|
|
4285
|
-
* element in `root` from a previous renderer call for reusing it. Even though combo-box uses
|
|
4286
|
-
* infinite scrolling, reducing DOM operations might improve performance.
|
|
4287
|
-
*
|
|
4288
|
-
* The following properties are available in the `model` argument:
|
|
4289
|
-
*
|
|
4290
|
-
* Property | Type | Description
|
|
4291
|
-
* -----------|------------------|-------------
|
|
4292
|
-
* `index` | Number | Index of the item in the `items` array
|
|
4293
|
-
* `item` | String or Object | The item reference
|
|
4294
|
-
* `selected` | Boolean | True when item is selected
|
|
4295
|
-
* `focused` | Boolean | True when item is focused
|
|
4296
|
-
*
|
|
4297
|
-
* ### Lazy Loading with Function Data Provider
|
|
4298
|
-
*
|
|
4299
|
-
* In addition to assigning an array to the items property, you can alternatively use the
|
|
4300
|
-
* [`dataProvider`](#/elements/vaadin-combo-box#property-dataProvider) function property.
|
|
4301
|
-
* The `<vaadin-combo-box>` calls this function lazily, only when it needs more data
|
|
4302
|
-
* to be displayed.
|
|
4303
|
-
*
|
|
4304
|
-
* __Note that when using function data providers, the total number of items
|
|
4305
|
-
* needs to be set manually. The total number of items can be returned
|
|
4306
|
-
* in the second argument of the data provider callback:__
|
|
4307
|
-
*
|
|
4308
|
-
* ```js
|
|
4309
|
-
* comboBox.dataProvider = async (params, callback) => {
|
|
4310
|
-
* const API = 'https://demo.vaadin.com/demo-data/1.0/filtered-countries';
|
|
4311
|
-
* const { filter, page, pageSize } = params;
|
|
4312
|
-
* const index = page * pageSize;
|
|
4313
|
-
*
|
|
4314
|
-
* const res = await fetch(`${API}?index=${index}&count=${pageSize}&filter=${filter}`);
|
|
4315
|
-
* if (res.ok) {
|
|
4316
|
-
* const { result, size } = await res.json();
|
|
4317
|
-
* callback(result, size);
|
|
4318
|
-
* }
|
|
4319
|
-
* };
|
|
4320
|
-
* ```
|
|
4321
|
-
*
|
|
4322
|
-
* ### Styling
|
|
4323
|
-
*
|
|
4324
|
-
* The following custom properties are available for styling:
|
|
4325
|
-
*
|
|
4326
|
-
* Custom property | Description | Default
|
|
4327
|
-
* ----------------------------------------|----------------------------|---------
|
|
4328
|
-
* `--vaadin-field-default-width` | Default width of the field | `12em`
|
|
4329
|
-
* `--vaadin-combo-box-overlay-width` | Width of the overlay | `auto`
|
|
4330
|
-
* `--vaadin-combo-box-overlay-max-height` | Max height of the overlay | `65vh`
|
|
4331
|
-
*
|
|
4332
|
-
* `<vaadin-combo-box>` provides the same set of shadow DOM parts and state attributes as `<vaadin-text-field>`.
|
|
4333
|
-
* See [`<vaadin-text-field>`](#/elements/vaadin-text-field) for the styling documentation.
|
|
4334
|
-
*
|
|
4335
|
-
* In addition to `<vaadin-text-field>` parts, the following parts are available for theming:
|
|
4336
|
-
*
|
|
4337
|
-
* Part name | Description
|
|
4338
|
-
* ----------------|----------------
|
|
4339
|
-
* `toggle-button` | The toggle button
|
|
4340
|
-
*
|
|
4341
|
-
* In addition to `<vaadin-text-field>` state attributes, the following state attributes are available for theming:
|
|
4342
|
-
*
|
|
4343
|
-
* Attribute | Description | Part name
|
|
4344
|
-
* ----------|-------------|------------
|
|
4345
|
-
* `opened` | Set when the combo box dropdown is open | :host
|
|
4346
|
-
* `loading` | Set when new items are expected | :host
|
|
4347
|
-
*
|
|
4348
|
-
* If you want to replace the default `<input>` and its container with a custom implementation to get full control
|
|
4349
|
-
* over the input field, consider using the [`<vaadin-combo-box-light>`](#/elements/vaadin-combo-box-light) element.
|
|
4350
|
-
*
|
|
4351
|
-
* ### Internal components
|
|
4352
|
-
*
|
|
4353
|
-
* In addition to `<vaadin-combo-box>` itself, the following internal
|
|
4354
|
-
* components are themable:
|
|
4355
|
-
*
|
|
4356
|
-
* - `<vaadin-combo-box-overlay>` - has the same API as [`<vaadin-overlay>`](#/elements/vaadin-overlay).
|
|
4357
|
-
* - `<vaadin-combo-box-item>` - has the same API as [`<vaadin-item>`](#/elements/vaadin-item).
|
|
4358
|
-
* - [`<vaadin-input-container>`](#/elements/vaadin-input-container) - an internal element wrapping the input.
|
|
4359
|
-
*
|
|
4360
|
-
* Note: the `theme` attribute value set on `<vaadin-combo-box>` is
|
|
4361
|
-
* propagated to the internal components listed above.
|
|
4362
|
-
*
|
|
4363
|
-
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
|
|
4364
|
-
*
|
|
4365
|
-
* @fires {Event} change - Fired when the user commits a value change.
|
|
4366
|
-
* @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
|
|
4367
|
-
* @fires {CustomEvent} filter-changed - Fired when the `filter` property changes.
|
|
4368
|
-
* @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
|
|
4369
|
-
* @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
|
|
4370
|
-
* @fires {CustomEvent} selected-item-changed - Fired when the `selectedItem` property changes.
|
|
4371
|
-
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
|
|
4372
|
-
* @fires {CustomEvent} validated - Fired whenever the field is validated.
|
|
4373
|
-
*
|
|
4374
|
-
* @customElement
|
|
4375
|
-
* @extends HTMLElement
|
|
4376
|
-
* @mixes ElementMixin
|
|
4377
|
-
* @mixes ThemableMixin
|
|
4378
|
-
* @mixes InputControlMixin
|
|
4379
|
-
* @mixes PatternMixin
|
|
4380
|
-
* @mixes ComboBoxDataProviderMixin
|
|
4381
|
-
* @mixes ComboBoxMixin
|
|
4382
|
-
*/
|
|
4383
|
-
class ComboBox extends ComboBoxDataProviderMixin(
|
|
4384
|
-
ComboBoxMixin(PatternMixin(InputControlMixin(ThemableMixin(ElementMixin(PolymerElement))))),
|
|
4385
|
-
) {
|
|
4386
|
-
static get is() {
|
|
4387
|
-
return 'vaadin-combo-box';
|
|
4388
|
-
}
|
|
4389
|
-
|
|
4390
|
-
static get template() {
|
|
4391
|
-
return html`
|
|
4392
|
-
<style>
|
|
4393
|
-
:host([opened]) {
|
|
4394
|
-
pointer-events: auto;
|
|
4395
|
-
}
|
|
4396
|
-
</style>
|
|
4397
|
-
|
|
4398
|
-
<div class="vaadin-combo-box-container">
|
|
4399
|
-
<div part="label">
|
|
4400
|
-
<slot name="label"></slot>
|
|
4401
|
-
<span part="required-indicator" aria-hidden="true" on-click="focus"></span>
|
|
4402
|
-
</div>
|
|
4403
|
-
|
|
4404
|
-
<vaadin-input-container
|
|
4405
|
-
part="input-field"
|
|
4406
|
-
readonly="[[readonly]]"
|
|
4407
|
-
disabled="[[disabled]]"
|
|
4408
|
-
invalid="[[invalid]]"
|
|
4409
|
-
theme$="[[_theme]]"
|
|
4410
|
-
>
|
|
4411
|
-
<slot name="prefix" slot="prefix"></slot>
|
|
4412
|
-
<slot name="input"></slot>
|
|
4413
|
-
<div id="clearButton" part="clear-button" slot="suffix" aria-hidden="true"></div>
|
|
4414
|
-
<div id="toggleButton" part="toggle-button" slot="suffix" aria-hidden="true"></div>
|
|
4415
|
-
</vaadin-input-container>
|
|
4416
|
-
|
|
4417
|
-
<div part="helper-text">
|
|
4418
|
-
<slot name="helper"></slot>
|
|
4419
|
-
</div>
|
|
4420
|
-
|
|
4421
|
-
<div part="error-message">
|
|
4422
|
-
<slot name="error-message"></slot>
|
|
4423
|
-
</div>
|
|
4424
|
-
</div>
|
|
4425
|
-
|
|
4426
|
-
<vaadin-combo-box-overlay
|
|
4427
|
-
id="overlay"
|
|
4428
|
-
opened="[[_overlayOpened]]"
|
|
4429
|
-
loading$="[[loading]]"
|
|
4430
|
-
theme$="[[_theme]]"
|
|
4431
|
-
position-target="[[_positionTarget]]"
|
|
4432
|
-
no-vertical-overlap
|
|
4433
|
-
restore-focus-node="[[inputElement]]"
|
|
4434
|
-
></vaadin-combo-box-overlay>
|
|
4435
|
-
|
|
4436
|
-
<slot name="tooltip"></slot>
|
|
4437
|
-
`;
|
|
4438
|
-
}
|
|
4439
|
-
|
|
4440
|
-
static get properties() {
|
|
4441
|
-
return {
|
|
4442
|
-
/**
|
|
4443
|
-
* @protected
|
|
4444
|
-
*/
|
|
4445
|
-
_positionTarget: {
|
|
4446
|
-
type: Object,
|
|
4447
|
-
},
|
|
4448
|
-
};
|
|
4449
|
-
}
|
|
4450
|
-
|
|
4451
|
-
/**
|
|
4452
|
-
* Used by `InputControlMixin` as a reference to the clear button element.
|
|
4453
|
-
* @protected
|
|
4454
|
-
* @return {!HTMLElement}
|
|
4455
|
-
*/
|
|
4456
|
-
get clearElement() {
|
|
4457
|
-
return this.$.clearButton;
|
|
4458
|
-
}
|
|
4459
|
-
|
|
4460
|
-
/** @protected */
|
|
4461
|
-
ready() {
|
|
4462
|
-
super.ready();
|
|
4463
|
-
|
|
4464
|
-
this.addController(
|
|
4465
|
-
new InputController(this, (input) => {
|
|
4466
|
-
this._setInputElement(input);
|
|
4467
|
-
this._setFocusElement(input);
|
|
4468
|
-
this.stateTarget = input;
|
|
4469
|
-
this.ariaTarget = input;
|
|
4470
|
-
}),
|
|
4471
|
-
);
|
|
4472
|
-
this.addController(new LabelledInputController(this.inputElement, this._labelController));
|
|
4473
|
-
|
|
4474
|
-
this._tooltipController = new TooltipController(this);
|
|
4475
|
-
this.addController(this._tooltipController);
|
|
4476
|
-
this._tooltipController.setPosition('top');
|
|
4477
|
-
this._tooltipController.setAriaTarget(this.inputElement);
|
|
4478
|
-
this._tooltipController.setShouldShow((target) => !target.opened);
|
|
4479
|
-
|
|
4480
|
-
this._positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
|
|
4481
|
-
this._toggleElement = this.$.toggleButton;
|
|
4482
|
-
}
|
|
4483
|
-
|
|
4484
|
-
/**
|
|
4485
|
-
* Override the method from `InputControlMixin`
|
|
4486
|
-
* to stop event propagation to prevent `ComboBoxMixin`
|
|
4487
|
-
* from handling this click event also on its own.
|
|
4488
|
-
*
|
|
4489
|
-
* @param {Event} event
|
|
4490
|
-
* @protected
|
|
4491
|
-
* @override
|
|
4492
|
-
*/
|
|
4493
|
-
_onClearButtonClick(event) {
|
|
4494
|
-
event.stopPropagation();
|
|
4495
|
-
super._onClearButtonClick(event);
|
|
4496
|
-
}
|
|
4497
|
-
|
|
4498
|
-
/**
|
|
4499
|
-
* @param {Event} event
|
|
4500
|
-
* @protected
|
|
4501
|
-
*/
|
|
4502
|
-
_onHostClick(event) {
|
|
4503
|
-
const path = event.composedPath();
|
|
4504
|
-
|
|
4505
|
-
// Open dropdown only when clicking on the label or input field
|
|
4506
|
-
if (path.includes(this._labelNode) || path.includes(this._positionTarget)) {
|
|
4507
|
-
super._onHostClick(event);
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
}
|
|
4511
|
-
|
|
4512
|
-
defineCustomElement(ComboBox);
|