@everymatrix/general-input 1.43.4 → 1.45.0

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.
Files changed (138) hide show
  1. package/dist/cjs/app-globals-3a1e7e63.js +5 -0
  2. package/dist/cjs/checkbox-group-input_10.cjs.entry.js +3873 -1756
  3. package/dist/cjs/general-input.cjs.entry.js +65 -62
  4. package/dist/cjs/general-input.cjs.js +17 -11
  5. package/dist/cjs/index-8cb018cb.js +1316 -0
  6. package/dist/cjs/index.cjs.js +13 -13
  7. package/dist/cjs/loader.cjs.js +7 -13
  8. package/dist/cjs/locale.utils-fdc29445.js +147 -0
  9. package/dist/cjs/toggle-checkbox-input.cjs.entry.js +77 -74
  10. package/dist/collection/collection-manifest.json +14 -14
  11. package/dist/collection/components/checkbox-group-input/checkbox-group-input.js +368 -353
  12. package/dist/collection/components/checkbox-input/checkbox-input.js +325 -315
  13. package/dist/collection/components/date-input/date-input.css +2 -2
  14. package/dist/collection/components/date-input/date-input.js +397 -376
  15. package/dist/collection/components/email-input/email-input.css +7 -11
  16. package/dist/collection/components/email-input/email-input.js +404 -385
  17. package/dist/collection/components/general-input/general-input.js +373 -367
  18. package/dist/collection/components/general-input/index.js +1 -0
  19. package/dist/collection/components/number-input/number-input.js +370 -352
  20. package/dist/collection/components/password-input/password-input.css +2 -4
  21. package/dist/collection/components/password-input/password-input.js +513 -540
  22. package/dist/collection/components/radio-input/radio-input.js +301 -286
  23. package/dist/collection/components/select-input/select-input.css +8 -9
  24. package/dist/collection/components/select-input/select-input.js +427 -414
  25. package/dist/collection/components/tel-input/tel-input.css +1 -1
  26. package/dist/collection/components/tel-input/tel-input.js +440 -422
  27. package/dist/collection/components/text-input/text-input.css +0 -1
  28. package/dist/collection/components/text-input/text-input.js +444 -429
  29. package/dist/collection/components/toggle-checkbox-input/toggle-checkbox-input.js +327 -318
  30. package/dist/collection/index.js +13 -13
  31. package/dist/collection/utils/locale.utils.js +133 -133
  32. package/dist/collection/utils/utils.js +3 -3
  33. package/dist/esm/app-globals-0f993ce5.js +3 -0
  34. package/dist/esm/checkbox-group-input_10.entry.js +3873 -1756
  35. package/dist/esm/general-input.entry.js +65 -62
  36. package/dist/esm/general-input.js +14 -11
  37. package/dist/esm/index-514fda47.js +1287 -0
  38. package/dist/esm/index.js +13 -13
  39. package/dist/esm/loader.js +7 -13
  40. package/dist/esm/locale.utils-75b7d185.js +144 -0
  41. package/dist/esm/toggle-checkbox-input.entry.js +77 -74
  42. package/dist/general-input/general-input.esm.js +1 -1
  43. package/dist/general-input/p-03e81c11.js +2 -0
  44. package/dist/general-input/p-122566dd.entry.js +5430 -0
  45. package/dist/general-input/p-1913dcb0.entry.js +1 -0
  46. package/dist/general-input/p-e1255160.js +1 -0
  47. package/dist/general-input/p-e7df9c4a.entry.js +1 -0
  48. package/dist/stencil.config.dev.js +17 -0
  49. package/dist/stencil.config.js +14 -19
  50. package/dist/types/Users/adrian.pripon/Documents/Work/widgets-monorepo/packages/stencil/general-input/.stencil/packages/stencil/general-input/stencil.config.d.ts +2 -0
  51. package/dist/types/Users/adrian.pripon/Documents/Work/widgets-monorepo/packages/stencil/general-input/.stencil/packages/stencil/general-input/stencil.config.dev.d.ts +2 -0
  52. package/dist/types/components/checkbox-group-input/checkbox-group-input.d.ts +68 -68
  53. package/dist/types/components/checkbox-input/checkbox-input.d.ts +61 -62
  54. package/dist/types/components/date-input/date-input.d.ts +78 -78
  55. package/dist/types/components/email-input/email-input.d.ts +77 -77
  56. package/dist/types/components/general-input/general-input.d.ts +72 -72
  57. package/dist/types/components/general-input/index.d.ts +1 -0
  58. package/dist/types/components/number-input/number-input.d.ts +71 -71
  59. package/dist/types/components/password-input/password-input.d.ts +87 -92
  60. package/dist/types/components/radio-input/radio-input.d.ts +55 -55
  61. package/dist/types/components/select-input/select-input.d.ts +79 -79
  62. package/dist/types/components/tel-input/tel-input.d.ts +85 -85
  63. package/dist/types/components/text-input/text-input.d.ts +81 -81
  64. package/dist/types/components/toggle-checkbox-input/toggle-checkbox-input.d.ts +63 -64
  65. package/dist/types/components.d.ts +201 -31
  66. package/dist/types/stencil-public-runtime.d.ts +142 -33
  67. package/dist/types/utils/locale.utils.d.ts +8 -8
  68. package/dist/types/utils/types.d.ts +54 -54
  69. package/loader/cdn.js +1 -3
  70. package/loader/index.cjs.js +1 -3
  71. package/loader/index.d.ts +13 -1
  72. package/loader/index.es2017.js +1 -3
  73. package/loader/index.js +1 -3
  74. package/loader/package.json +1 -0
  75. package/package.json +8 -1
  76. package/dist/cjs/index-132a0774.js +0 -1327
  77. package/dist/cjs/locale.utils-2fa6f747.js +0 -147
  78. package/dist/components/active-mixin.js +0 -975
  79. package/dist/components/checkbox-group-input.d.ts +0 -11
  80. package/dist/components/checkbox-group-input.js +0 -6
  81. package/dist/components/checkbox-group-input2.js +0 -1078
  82. package/dist/components/checkbox-input.d.ts +0 -11
  83. package/dist/components/checkbox-input.js +0 -6
  84. package/dist/components/checkbox-input2.js +0 -132
  85. package/dist/components/date-input.d.ts +0 -11
  86. package/dist/components/date-input.js +0 -6
  87. package/dist/components/date-input2.js +0 -11556
  88. package/dist/components/email-input.d.ts +0 -11
  89. package/dist/components/email-input.js +0 -6
  90. package/dist/components/email-input2.js +0 -171
  91. package/dist/components/field-mixin.js +0 -12426
  92. package/dist/components/general-input.d.ts +0 -11
  93. package/dist/components/general-input.js +0 -6
  94. package/dist/components/general-input2.js +0 -344
  95. package/dist/components/index.d.ts +0 -26
  96. package/dist/components/index.js +0 -18
  97. package/dist/components/input-field-shared-styles.js +0 -1211
  98. package/dist/components/number-input.d.ts +0 -11
  99. package/dist/components/number-input.js +0 -6
  100. package/dist/components/number-input2.js +0 -158
  101. package/dist/components/password-input.d.ts +0 -11
  102. package/dist/components/password-input.js +0 -6
  103. package/dist/components/password-input2.js +0 -1059
  104. package/dist/components/radio-input.d.ts +0 -11
  105. package/dist/components/radio-input.js +0 -6
  106. package/dist/components/radio-input2.js +0 -114
  107. package/dist/components/select-input.d.ts +0 -11
  108. package/dist/components/select-input.js +0 -6
  109. package/dist/components/select-input2.js +0 -183
  110. package/dist/components/tel-input.d.ts +0 -11
  111. package/dist/components/tel-input.js +0 -6
  112. package/dist/components/tel-input2.js +0 -197
  113. package/dist/components/text-input.d.ts +0 -11
  114. package/dist/components/text-input.js +0 -6
  115. package/dist/components/text-input2.js +0 -199
  116. package/dist/components/toggle-checkbox-input.d.ts +0 -11
  117. package/dist/components/toggle-checkbox-input.js +0 -6
  118. package/dist/components/tooltipIcon.js +0 -146
  119. package/dist/components/vaadin-button.js +0 -490
  120. package/dist/components/vaadin-combo-box.js +0 -4512
  121. package/dist/components/virtual-keyboard-controller.js +0 -2001
  122. package/dist/esm/index-db76d5b5.js +0 -1299
  123. package/dist/esm/locale.utils-30fb5289.js +0 -144
  124. package/dist/esm/polyfills/core-js.js +0 -11
  125. package/dist/esm/polyfills/css-shim.js +0 -1
  126. package/dist/esm/polyfills/dom.js +0 -79
  127. package/dist/esm/polyfills/es5-html-element.js +0 -1
  128. package/dist/esm/polyfills/index.js +0 -34
  129. package/dist/esm/polyfills/system.js +0 -6
  130. package/dist/general-input/p-6ae987d0.entry.js +0 -1
  131. package/dist/general-input/p-b408093e.js +0 -1
  132. package/dist/general-input/p-b64caafa.entry.js +0 -3646
  133. package/dist/general-input/p-f659e9ee.entry.js +0 -1
  134. package/dist/types/Users/adrian.pripon/Documents/Work/widgets-stencil/packages/general-input/.stencil/packages/general-input/stencil.config.d.ts +0 -2
  135. /package/dist/cjs/{tooltipIcon-092a795f.js → tooltipIcon-7e9ee226.js} +0 -0
  136. /package/dist/esm/{tooltipIcon-99c1c7b7.js → tooltipIcon-0a5a06a2.js} +0 -0
  137. /package/dist/general-input/{p-f4f4ccda.js → p-2dccd0bf.js} +0 -0
  138. /package/dist/general-input/{p-04d4b145.js → p-ba14eb83.js} +0 -0
@@ -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);