@everymatrix/general-input 0.0.1

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