@vaadin/component-base 24.0.0-alpha3 → 24.0.0-alpha5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/dir-mixin.d.ts +2 -0
- package/src/dir-mixin.js +8 -0
- package/src/element-mixin.js +1 -1
- package/src/list-mixin.d.ts +50 -0
- package/src/list-mixin.js +344 -0
- package/src/slot-controller.d.ts +16 -0
- package/src/slot-controller.js +77 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/component-base",
|
|
3
|
-
"version": "24.0.0-
|
|
3
|
+
"version": "24.0.0-alpha5",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"@vaadin/testing-helpers": "^0.3.2",
|
|
43
43
|
"sinon": "^13.0.2"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "fc0b1721eda9e39cb289b239e440fc9e29573a31"
|
|
46
46
|
}
|
package/src/dir-mixin.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
|
11
11
|
export declare function DirMixin<T extends Constructor<HTMLElement>>(base: T): Constructor<DirMixinClass> & T;
|
|
12
12
|
|
|
13
13
|
export declare class DirMixinClass {
|
|
14
|
+
protected readonly __isRTL: boolean;
|
|
15
|
+
|
|
14
16
|
protected __getNormalizedScrollLeft(element: Element | null): number;
|
|
15
17
|
|
|
16
18
|
protected __setNormalizedScrollLeft(element: Element | null, scrollLeft: number): void;
|
package/src/dir-mixin.js
CHANGED
|
@@ -112,6 +112,14 @@ export const DirMixin = (superClass) =>
|
|
|
112
112
|
this.__unsubscribe();
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* @return {boolean}
|
|
117
|
+
* @protected
|
|
118
|
+
*/
|
|
119
|
+
get __isRTL() {
|
|
120
|
+
return this.getAttribute('dir') === 'rtl';
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
/** @protected */
|
|
116
124
|
_valueToNodeAttribute(node, value, attribute) {
|
|
117
125
|
// Override default Polymer attribute reflection to match native behavior of HTMLElement.dir property
|
package/src/element-mixin.js
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import type { KeyboardDirectionMixinClass } from './keyboard-direction-mixin.js';
|
|
8
|
+
import type { KeyboardMixinClass } from './keyboard-mixin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A mixin for list elements, facilitating navigation and selection of items.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ListMixin<T extends Constructor<HTMLElement>>(
|
|
14
|
+
base: T,
|
|
15
|
+
): Constructor<KeyboardDirectionMixinClass> & Constructor<KeyboardMixinClass> & Constructor<ListMixinClass> & T;
|
|
16
|
+
|
|
17
|
+
export declare class ListMixinClass {
|
|
18
|
+
/**
|
|
19
|
+
* Used for mixin detection because `instanceof` does not work with mixins.
|
|
20
|
+
*/
|
|
21
|
+
_hasVaadinListMixin: boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The index of the item selected in the items array.
|
|
25
|
+
* Note: Not updated when used in `multiple` selection mode.
|
|
26
|
+
*/
|
|
27
|
+
selected: number | null | undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Define how items are disposed in the dom.
|
|
31
|
+
* Possible values are: `horizontal|vertical`.
|
|
32
|
+
* It also changes navigation keys from left/right to up/down.
|
|
33
|
+
*/
|
|
34
|
+
orientation: 'horizontal' | 'vertical';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The list of items from which a selection can be made.
|
|
38
|
+
* It is populated from the elements passed to the light DOM,
|
|
39
|
+
* and updated dynamically when adding or removing items.
|
|
40
|
+
*
|
|
41
|
+
* The item elements must implement `Vaadin.ItemMixin`.
|
|
42
|
+
*
|
|
43
|
+
* Note: unlike `<vaadin-combo-box>`, this property is read-only,
|
|
44
|
+
* so if you want to provide items by iterating array of data,
|
|
45
|
+
* you have to use `dom-repeat` and place it to the light DOM.
|
|
46
|
+
*/
|
|
47
|
+
readonly items: Element[] | undefined;
|
|
48
|
+
|
|
49
|
+
protected readonly _scrollerElement: HTMLElement;
|
|
50
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
|
|
7
|
+
import { timeOut } from './async.js';
|
|
8
|
+
import { Debouncer } from './debounce.js';
|
|
9
|
+
import { DirHelper } from './dir-helper.js';
|
|
10
|
+
import { KeyboardDirectionMixin } from './keyboard-direction-mixin.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A mixin for list elements, facilitating navigation and selection of items.
|
|
14
|
+
*
|
|
15
|
+
* @polymerMixin
|
|
16
|
+
* @mixes KeyboardDirectionMixin
|
|
17
|
+
*/
|
|
18
|
+
export const ListMixin = (superClass) =>
|
|
19
|
+
class ListMixinClass extends KeyboardDirectionMixin(superClass) {
|
|
20
|
+
static get properties() {
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* Used for mixin detection because `instanceof` does not work with mixins.
|
|
24
|
+
* @type {boolean}
|
|
25
|
+
*/
|
|
26
|
+
_hasVaadinListMixin: {
|
|
27
|
+
value: true,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The index of the item selected in the items array.
|
|
32
|
+
* Note: Not updated when used in `multiple` selection mode.
|
|
33
|
+
*/
|
|
34
|
+
selected: {
|
|
35
|
+
type: Number,
|
|
36
|
+
reflectToAttribute: true,
|
|
37
|
+
notify: true,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Define how items are disposed in the dom.
|
|
42
|
+
* Possible values are: `horizontal|vertical`.
|
|
43
|
+
* It also changes navigation keys from left/right to up/down.
|
|
44
|
+
* @type {!ListOrientation}
|
|
45
|
+
*/
|
|
46
|
+
orientation: {
|
|
47
|
+
type: String,
|
|
48
|
+
reflectToAttribute: true,
|
|
49
|
+
value: '',
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The list of items from which a selection can be made.
|
|
54
|
+
* It is populated from the elements passed to the light DOM,
|
|
55
|
+
* and updated dynamically when adding or removing items.
|
|
56
|
+
*
|
|
57
|
+
* The item elements must implement `Vaadin.ItemMixin`.
|
|
58
|
+
*
|
|
59
|
+
* Note: unlike `<vaadin-combo-box>`, this property is read-only,
|
|
60
|
+
* so if you want to provide items by iterating array of data,
|
|
61
|
+
* you have to use `dom-repeat` and place it to the light DOM.
|
|
62
|
+
* @type {!Array<!Element> | undefined}
|
|
63
|
+
*/
|
|
64
|
+
items: {
|
|
65
|
+
type: Array,
|
|
66
|
+
readOnly: true,
|
|
67
|
+
notify: true,
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The search buffer for the keyboard selection feature.
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
_searchBuf: {
|
|
75
|
+
type: String,
|
|
76
|
+
value: '',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static get observers() {
|
|
82
|
+
return ['_enhanceItems(items, orientation, selected, disabled)'];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @protected */
|
|
86
|
+
ready() {
|
|
87
|
+
super.ready();
|
|
88
|
+
|
|
89
|
+
this.addEventListener('click', (e) => this._onClick(e));
|
|
90
|
+
|
|
91
|
+
this._observer = new FlattenedNodesObserver(this, () => {
|
|
92
|
+
this._setItems(this._filterItems(FlattenedNodesObserver.getFlattenedNodes(this)));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Override method inherited from `KeyboardDirectionMixin`
|
|
98
|
+
* to use the stored list of item elements.
|
|
99
|
+
*
|
|
100
|
+
* @return {Element[]}
|
|
101
|
+
* @protected
|
|
102
|
+
* @override
|
|
103
|
+
*/
|
|
104
|
+
_getItems() {
|
|
105
|
+
return this.items;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @private */
|
|
109
|
+
_enhanceItems(items, orientation, selected, disabled) {
|
|
110
|
+
if (!disabled) {
|
|
111
|
+
if (items) {
|
|
112
|
+
this.setAttribute('aria-orientation', orientation || 'vertical');
|
|
113
|
+
this.items.forEach((item) => {
|
|
114
|
+
if (orientation) {
|
|
115
|
+
item.setAttribute('orientation', orientation);
|
|
116
|
+
} else {
|
|
117
|
+
item.removeAttribute('orientation');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this._setFocusable(selected || 0);
|
|
122
|
+
|
|
123
|
+
const itemToSelect = items[selected];
|
|
124
|
+
items.forEach((item) => {
|
|
125
|
+
item.selected = item === itemToSelect;
|
|
126
|
+
});
|
|
127
|
+
if (itemToSelect && !itemToSelect.disabled) {
|
|
128
|
+
this._scrollToItem(selected);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {!Array<!Element>} array
|
|
136
|
+
* @return {!Array<!Element>}
|
|
137
|
+
* @protected
|
|
138
|
+
*/
|
|
139
|
+
_filterItems(array) {
|
|
140
|
+
return array.filter((e) => e._hasVaadinItemMixin);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {!MouseEvent} event
|
|
145
|
+
* @protected
|
|
146
|
+
*/
|
|
147
|
+
_onClick(event) {
|
|
148
|
+
if (event.metaKey || event.shiftKey || event.ctrlKey || event.defaultPrevented) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const item = this._filterItems(event.composedPath())[0];
|
|
153
|
+
let idx;
|
|
154
|
+
if (item && !item.disabled && (idx = this.items.indexOf(item)) >= 0) {
|
|
155
|
+
this.selected = idx;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {number} currentIdx
|
|
161
|
+
* @param {string} key
|
|
162
|
+
* @return {number}
|
|
163
|
+
* @protected
|
|
164
|
+
*/
|
|
165
|
+
_searchKey(currentIdx, key) {
|
|
166
|
+
this._searchReset = Debouncer.debounce(this._searchReset, timeOut.after(500), () => {
|
|
167
|
+
this._searchBuf = '';
|
|
168
|
+
});
|
|
169
|
+
this._searchBuf += key.toLowerCase();
|
|
170
|
+
|
|
171
|
+
if (!this.items.some((item) => this.__isMatchingKey(item))) {
|
|
172
|
+
this._searchBuf = key.toLowerCase();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const idx = this._searchBuf.length === 1 ? currentIdx + 1 : currentIdx;
|
|
176
|
+
return this._getAvailableIndex(
|
|
177
|
+
this.items,
|
|
178
|
+
idx,
|
|
179
|
+
1,
|
|
180
|
+
(item) => this.__isMatchingKey(item) && getComputedStyle(item).display !== 'none',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @private */
|
|
185
|
+
__isMatchingKey(item) {
|
|
186
|
+
return item.textContent
|
|
187
|
+
.replace(/[^\p{L}\p{Nd}]/gu, '')
|
|
188
|
+
.toLowerCase()
|
|
189
|
+
.startsWith(this._searchBuf);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @return {boolean}
|
|
194
|
+
* @protected
|
|
195
|
+
*/
|
|
196
|
+
get _isRTL() {
|
|
197
|
+
return !this._vertical && this.getAttribute('dir') === 'rtl';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Override an event listener from `KeyboardMixin`
|
|
202
|
+
* to search items by key.
|
|
203
|
+
*
|
|
204
|
+
* @param {!KeyboardEvent} event
|
|
205
|
+
* @protected
|
|
206
|
+
* @override
|
|
207
|
+
*/
|
|
208
|
+
_onKeyDown(event) {
|
|
209
|
+
if (event.metaKey || event.ctrlKey) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const key = event.key;
|
|
214
|
+
|
|
215
|
+
const currentIdx = this.items.indexOf(this.focused);
|
|
216
|
+
if (/[a-zA-Z0-9]/.test(key) && key.length === 1) {
|
|
217
|
+
const idx = this._searchKey(currentIdx, key);
|
|
218
|
+
if (idx >= 0) {
|
|
219
|
+
this._focus(idx);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
super._onKeyDown(event);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {!Element} item
|
|
229
|
+
* @return {boolean}
|
|
230
|
+
* @protected
|
|
231
|
+
*/
|
|
232
|
+
_isItemHidden(item) {
|
|
233
|
+
return getComputedStyle(item).display === 'none';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @param {number} idx
|
|
238
|
+
* @protected
|
|
239
|
+
*/
|
|
240
|
+
_setFocusable(idx) {
|
|
241
|
+
idx = this._getAvailableIndex(this.items, idx, 1);
|
|
242
|
+
const item = this.items[idx];
|
|
243
|
+
this.items.forEach((e) => {
|
|
244
|
+
e.tabIndex = e === item ? 0 : -1;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {number} idx
|
|
250
|
+
* @protected
|
|
251
|
+
*/
|
|
252
|
+
_focus(idx) {
|
|
253
|
+
this.items.forEach((e, index) => {
|
|
254
|
+
e.focused = index === idx;
|
|
255
|
+
});
|
|
256
|
+
this._setFocusable(idx);
|
|
257
|
+
this._scrollToItem(idx);
|
|
258
|
+
super._focus(idx);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
focus() {
|
|
262
|
+
// In initialization (e.g vaadin-select) observer might not been run yet.
|
|
263
|
+
if (this._observer) {
|
|
264
|
+
this._observer.flush();
|
|
265
|
+
}
|
|
266
|
+
const firstItem = this.querySelector('[tabindex="0"]') || (this.items ? this.items[0] : null);
|
|
267
|
+
this._focusItem(firstItem);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @return {!HTMLElement}
|
|
272
|
+
* @protected
|
|
273
|
+
*/
|
|
274
|
+
get _scrollerElement() {
|
|
275
|
+
// Returning scroller element of the component
|
|
276
|
+
console.warn(`Please implement the '_scrollerElement' property in <${this.localName}>`);
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Scroll the container to have the next item by the edge of the viewport.
|
|
282
|
+
* @param {number} idx
|
|
283
|
+
* @protected
|
|
284
|
+
*/
|
|
285
|
+
_scrollToItem(idx) {
|
|
286
|
+
const item = this.items[idx];
|
|
287
|
+
if (!item) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const props = this._vertical ? ['top', 'bottom'] : this._isRTL ? ['right', 'left'] : ['left', 'right'];
|
|
292
|
+
|
|
293
|
+
const scrollerRect = this._scrollerElement.getBoundingClientRect();
|
|
294
|
+
const nextItemRect = (this.items[idx + 1] || item).getBoundingClientRect();
|
|
295
|
+
const prevItemRect = (this.items[idx - 1] || item).getBoundingClientRect();
|
|
296
|
+
|
|
297
|
+
let scrollDistance = 0;
|
|
298
|
+
if (
|
|
299
|
+
(!this._isRTL && nextItemRect[props[1]] >= scrollerRect[props[1]]) ||
|
|
300
|
+
(this._isRTL && nextItemRect[props[1]] <= scrollerRect[props[1]])
|
|
301
|
+
) {
|
|
302
|
+
scrollDistance = nextItemRect[props[1]] - scrollerRect[props[1]];
|
|
303
|
+
} else if (
|
|
304
|
+
(!this._isRTL && prevItemRect[props[0]] <= scrollerRect[props[0]]) ||
|
|
305
|
+
(this._isRTL && prevItemRect[props[0]] >= scrollerRect[props[0]])
|
|
306
|
+
) {
|
|
307
|
+
scrollDistance = prevItemRect[props[0]] - scrollerRect[props[0]];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this._scroll(scrollDistance);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @return {boolean}
|
|
315
|
+
* @protected
|
|
316
|
+
*/
|
|
317
|
+
get _vertical() {
|
|
318
|
+
return this.orientation !== 'horizontal';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {number} pixels
|
|
323
|
+
* @protected
|
|
324
|
+
*/
|
|
325
|
+
_scroll(pixels) {
|
|
326
|
+
if (this._vertical) {
|
|
327
|
+
this._scrollerElement.scrollTop += pixels;
|
|
328
|
+
} else {
|
|
329
|
+
const dir = this.getAttribute('dir') || 'ltr';
|
|
330
|
+
const scrollType = DirHelper.detectScrollType();
|
|
331
|
+
const scrollLeft = DirHelper.getNormalizedScrollLeft(scrollType, dir, this._scrollerElement) + pixels;
|
|
332
|
+
DirHelper.setNormalizedScrollLeft(scrollType, dir, this._scrollerElement, scrollLeft);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Fired when the selection is changed.
|
|
338
|
+
* Not fired when used in `multiple` selection mode.
|
|
339
|
+
*
|
|
340
|
+
* @event selected-changed
|
|
341
|
+
* @param {Object} detail
|
|
342
|
+
* @param {Object} detail.value the index of the item selected in the items array.
|
|
343
|
+
*/
|
|
344
|
+
};
|
package/src/slot-controller.d.ts
CHANGED
|
@@ -16,8 +16,16 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
16
16
|
*/
|
|
17
17
|
node: HTMLElement;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The list of slotted nodes managed by the controller.
|
|
21
|
+
* Only used when `multiple` property is set to `true`.
|
|
22
|
+
*/
|
|
23
|
+
nodes: HTMLElement[];
|
|
24
|
+
|
|
19
25
|
protected initialized: boolean;
|
|
20
26
|
|
|
27
|
+
protected multiple: boolean;
|
|
28
|
+
|
|
21
29
|
protected defaultNode: Node;
|
|
22
30
|
|
|
23
31
|
protected defaultId: string;
|
|
@@ -27,6 +35,7 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
27
35
|
slotName: string,
|
|
28
36
|
tagName?: string,
|
|
29
37
|
config?: {
|
|
38
|
+
multiple?: boolean;
|
|
30
39
|
observe?: boolean;
|
|
31
40
|
useUniqueId?: boolean;
|
|
32
41
|
initializer?(host: HTMLElement, node: HTMLElement): void;
|
|
@@ -35,6 +44,11 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
35
44
|
|
|
36
45
|
hostConnected(): void;
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Return the list of nodes matching the slot managed by the controller.
|
|
49
|
+
*/
|
|
50
|
+
getSlotChildren(): Node[];
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* Return a reference to the node managed by the controller.
|
|
40
54
|
*/
|
|
@@ -42,6 +56,8 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
42
56
|
|
|
43
57
|
protected attachDefaultNode(): Node | undefined;
|
|
44
58
|
|
|
59
|
+
protected initAddedNode(node: Node): void;
|
|
60
|
+
|
|
45
61
|
protected initNode(node: Node): void;
|
|
46
62
|
|
|
47
63
|
/**
|
package/src/slot-controller.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
|
|
7
|
+
import { isEmptyTextNode } from './dom-utils.js';
|
|
7
8
|
import { generateUniqueId } from './unique-id-utils.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -26,14 +27,19 @@ export class SlotController extends EventTarget {
|
|
|
26
27
|
constructor(host, slotName, tagName, config = {}) {
|
|
27
28
|
super();
|
|
28
29
|
|
|
29
|
-
const { initializer, observe, useUniqueId } = config;
|
|
30
|
+
const { initializer, multiple, observe, useUniqueId } = config;
|
|
30
31
|
|
|
31
32
|
this.host = host;
|
|
32
33
|
this.slotName = slotName;
|
|
33
34
|
this.tagName = tagName;
|
|
34
35
|
this.observe = typeof observe === 'boolean' ? observe : true;
|
|
36
|
+
this.multiple = typeof multiple === 'boolean' ? multiple : false;
|
|
35
37
|
this.slotInitializer = initializer;
|
|
36
38
|
|
|
39
|
+
if (multiple) {
|
|
40
|
+
this.nodes = [];
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
// Only generate the default ID if requested by the controller.
|
|
38
44
|
if (useUniqueId) {
|
|
39
45
|
this.defaultId = SlotController.generateId(slotName, host);
|
|
@@ -42,17 +48,12 @@ export class SlotController extends EventTarget {
|
|
|
42
48
|
|
|
43
49
|
hostConnected() {
|
|
44
50
|
if (!this.initialized) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!node) {
|
|
48
|
-
node = this.attachDefaultNode();
|
|
51
|
+
if (this.multiple) {
|
|
52
|
+
this.initMultiple();
|
|
49
53
|
} else {
|
|
50
|
-
this.
|
|
51
|
-
this.initCustomNode(node);
|
|
54
|
+
this.initSingle();
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
this.initNode(node);
|
|
55
|
-
|
|
56
57
|
if (this.observe) {
|
|
57
58
|
this.observeSlot();
|
|
58
59
|
}
|
|
@@ -61,6 +62,35 @@ export class SlotController extends EventTarget {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/** @protected */
|
|
66
|
+
initSingle() {
|
|
67
|
+
let node = this.getSlotChild();
|
|
68
|
+
|
|
69
|
+
if (!node) {
|
|
70
|
+
node = this.attachDefaultNode();
|
|
71
|
+
this.initNode(node);
|
|
72
|
+
} else {
|
|
73
|
+
this.node = node;
|
|
74
|
+
this.initAddedNode(node);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @protected */
|
|
79
|
+
initMultiple() {
|
|
80
|
+
const children = this.getSlotChildren();
|
|
81
|
+
|
|
82
|
+
if (children.length === 0) {
|
|
83
|
+
const defaultNode = this.attachDefaultNode();
|
|
84
|
+
this.nodes = [defaultNode];
|
|
85
|
+
this.initNode(defaultNode);
|
|
86
|
+
} else {
|
|
87
|
+
this.nodes = children;
|
|
88
|
+
children.forEach((node) => {
|
|
89
|
+
this.initAddedNode(node);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
/**
|
|
65
95
|
* Create and attach default node using the slot factory.
|
|
66
96
|
* @return {Node | undefined}
|
|
@@ -92,12 +122,12 @@ export class SlotController extends EventTarget {
|
|
|
92
122
|
}
|
|
93
123
|
|
|
94
124
|
/**
|
|
95
|
-
* Return
|
|
125
|
+
* Return the list of nodes matching the slot managed by the controller.
|
|
96
126
|
* @return {Node}
|
|
97
127
|
*/
|
|
98
|
-
|
|
128
|
+
getSlotChildren() {
|
|
99
129
|
const { slotName } = this;
|
|
100
|
-
return Array.from(this.host.childNodes).
|
|
130
|
+
return Array.from(this.host.childNodes).filter((node) => {
|
|
101
131
|
// Either an element (any slot) or a text node (only un-named slot).
|
|
102
132
|
return (
|
|
103
133
|
(node.nodeType === Node.ELEMENT_NODE && node.slot === slotName) ||
|
|
@@ -106,6 +136,14 @@ export class SlotController extends EventTarget {
|
|
|
106
136
|
});
|
|
107
137
|
}
|
|
108
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Return a reference to the node managed by the controller.
|
|
141
|
+
* @return {Node}
|
|
142
|
+
*/
|
|
143
|
+
getSlotChild() {
|
|
144
|
+
return this.getSlotChildren()[0];
|
|
145
|
+
}
|
|
146
|
+
|
|
109
147
|
/**
|
|
110
148
|
* @param {Node} node
|
|
111
149
|
* @protected
|
|
@@ -135,6 +173,14 @@ export class SlotController extends EventTarget {
|
|
|
135
173
|
*/
|
|
136
174
|
teardownNode(_node) {}
|
|
137
175
|
|
|
176
|
+
/** @protected */
|
|
177
|
+
initAddedNode(node) {
|
|
178
|
+
if (node !== this.defaultNode) {
|
|
179
|
+
this.initCustomNode(node);
|
|
180
|
+
this.initNode(node);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
138
184
|
/**
|
|
139
185
|
* Setup the observer to manage slot content changes.
|
|
140
186
|
* @protected
|
|
@@ -145,9 +191,11 @@ export class SlotController extends EventTarget {
|
|
|
145
191
|
const slot = this.host.shadowRoot.querySelector(selector);
|
|
146
192
|
|
|
147
193
|
this.__slotObserver = new FlattenedNodesObserver(slot, (info) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
const current = this.multiple ? this.nodes : [this.node];
|
|
195
|
+
|
|
196
|
+
// Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
|
|
197
|
+
// unlike comment nodes, they are not filtered out. So we need to manually ignore them.
|
|
198
|
+
const newNodes = info.addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
|
|
151
199
|
|
|
152
200
|
if (info.removedNodes.length) {
|
|
153
201
|
info.removedNodes.forEach((node) => {
|
|
@@ -155,18 +203,22 @@ export class SlotController extends EventTarget {
|
|
|
155
203
|
});
|
|
156
204
|
}
|
|
157
205
|
|
|
158
|
-
if (
|
|
206
|
+
if (newNodes && newNodes.length > 0) {
|
|
159
207
|
// Custom node is added, remove the current one.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (newNode !== this.defaultNode) {
|
|
167
|
-
this.initCustomNode(newNode);
|
|
208
|
+
current.forEach((node) => {
|
|
209
|
+
if (node && node.isConnected) {
|
|
210
|
+
node.parentNode.removeChild(node);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
168
213
|
|
|
169
|
-
|
|
214
|
+
if (this.multiple) {
|
|
215
|
+
this.nodes = newNodes;
|
|
216
|
+
newNodes.forEach((node) => {
|
|
217
|
+
this.initAddedNode(node);
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
this.node = newNodes[0];
|
|
221
|
+
this.initAddedNode(this.node);
|
|
170
222
|
}
|
|
171
223
|
}
|
|
172
224
|
});
|