@everymatrix/general-registration 1.10.12 → 1.10.13

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