@everymatrix/general-registration 1.10.2 → 1.10.3

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