@vaadin/component-base 24.0.0-alpha4 → 24.0.0-alpha6
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/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 +9 -0
- package/src/slot-controller.js +15 -4
- package/src/slot-observe-controller.d.ts +28 -0
- package/src/slot-observe-controller.js +175 -0
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-alpha6",
|
|
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": "0004ac92b6e5f415b5fa949e0582d1d11e527b1f"
|
|
46
46
|
}
|
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
|
@@ -54,10 +54,19 @@ export class SlotController extends EventTarget implements ReactiveController {
|
|
|
54
54
|
*/
|
|
55
55
|
getSlotChild(): Node;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Create and attach default node using the provided tag name, if any.
|
|
59
|
+
*/
|
|
57
60
|
protected attachDefaultNode(): Node | undefined;
|
|
58
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Run both `initCustomNode` and `initNode` for a custom slotted node.
|
|
64
|
+
*/
|
|
59
65
|
protected initAddedNode(node: Node): void;
|
|
60
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Run `slotInitializer` for the node managed by the controller.
|
|
69
|
+
*/
|
|
61
70
|
protected initNode(node: Node): void;
|
|
62
71
|
|
|
63
72
|
/**
|
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
|
/**
|
|
@@ -91,7 +92,7 @@ export class SlotController extends EventTarget {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
* Create and attach default node using the
|
|
95
|
+
* Create and attach default node using the provided tag name, if any.
|
|
95
96
|
* @return {Node | undefined}
|
|
96
97
|
* @protected
|
|
97
98
|
*/
|
|
@@ -101,7 +102,7 @@ export class SlotController extends EventTarget {
|
|
|
101
102
|
// Check if the node was created previously and if so, reuse it.
|
|
102
103
|
let node = this.defaultNode;
|
|
103
104
|
|
|
104
|
-
//
|
|
105
|
+
// Tag name is optional, sometimes we don't init default content.
|
|
105
106
|
if (!node && tagName) {
|
|
106
107
|
node = document.createElement(tagName);
|
|
107
108
|
if (node instanceof Element) {
|
|
@@ -144,6 +145,8 @@ export class SlotController extends EventTarget {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
/**
|
|
148
|
+
* Run `slotInitializer` for the node managed by the controller.
|
|
149
|
+
*
|
|
147
150
|
* @param {Node} node
|
|
148
151
|
* @protected
|
|
149
152
|
*/
|
|
@@ -172,7 +175,12 @@ export class SlotController extends EventTarget {
|
|
|
172
175
|
*/
|
|
173
176
|
teardownNode(_node) {}
|
|
174
177
|
|
|
175
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Run both `initCustomNode` and `initNode` for a custom slotted node.
|
|
180
|
+
*
|
|
181
|
+
* @param {Node} node
|
|
182
|
+
* @protected
|
|
183
|
+
*/
|
|
176
184
|
initAddedNode(node) {
|
|
177
185
|
if (node !== this.defaultNode) {
|
|
178
186
|
this.initCustomNode(node);
|
|
@@ -191,7 +199,10 @@ export class SlotController extends EventTarget {
|
|
|
191
199
|
|
|
192
200
|
this.__slotObserver = new FlattenedNodesObserver(slot, (info) => {
|
|
193
201
|
const current = this.multiple ? this.nodes : [this.node];
|
|
194
|
-
|
|
202
|
+
|
|
203
|
+
// Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
|
|
204
|
+
// unlike comment nodes, they are not filtered out. So we need to manually ignore them.
|
|
205
|
+
const newNodes = info.addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
|
|
195
206
|
|
|
196
207
|
if (info.removedNodes.length) {
|
|
197
208
|
info.removedNodes.forEach((node) => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { SlotController } from './slot-controller.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A controller that observes slotted element mutations, especially ID attribute
|
|
10
|
+
* and the text content, and fires an event to notify host element about those.
|
|
11
|
+
*/
|
|
12
|
+
export class SlotObserveController extends SlotController {
|
|
13
|
+
/**
|
|
14
|
+
* Setup the mutation observer on the node to update ID and notify host.
|
|
15
|
+
* Node doesn't get observed automatically until this method is called.
|
|
16
|
+
*/
|
|
17
|
+
protected observeNode(node: Node): void;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Override to restore default node when a custom one is removed.
|
|
21
|
+
*/
|
|
22
|
+
protected restoreDefaultNode(): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Override to update default node text on property change.
|
|
26
|
+
*/
|
|
27
|
+
protected updateDefaultNode(node: Node): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { SlotController } from './slot-controller.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A controller that observes slotted element mutations, especially ID attribute
|
|
10
|
+
* and the text content, and fires an event to notify host element about those.
|
|
11
|
+
*/
|
|
12
|
+
export class SlotObserveController extends SlotController {
|
|
13
|
+
constructor(host, slot, tagName, config = {}) {
|
|
14
|
+
super(host, slot, tagName, { ...config, useUniqueId: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Override to initialize the newly added custom node.
|
|
19
|
+
*
|
|
20
|
+
* @param {Node} node
|
|
21
|
+
* @protected
|
|
22
|
+
* @override
|
|
23
|
+
*/
|
|
24
|
+
initCustomNode(node) {
|
|
25
|
+
this.__updateNodeId(node);
|
|
26
|
+
this.__notifyChange(node);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Override to notify the controller host about removal of
|
|
31
|
+
* the custom node, and to apply the default one if needed.
|
|
32
|
+
*
|
|
33
|
+
* @param {Node} _node
|
|
34
|
+
* @protected
|
|
35
|
+
* @override
|
|
36
|
+
*/
|
|
37
|
+
teardownNode(_node) {
|
|
38
|
+
const node = this.getSlotChild();
|
|
39
|
+
|
|
40
|
+
// Custom node is added to the slot
|
|
41
|
+
if (node && node !== this.defaultNode) {
|
|
42
|
+
this.__notifyChange(node);
|
|
43
|
+
} else {
|
|
44
|
+
this.restoreDefaultNode();
|
|
45
|
+
this.updateDefaultNode(this.node);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Override method inherited from `SlotMixin`
|
|
51
|
+
* to set ID attribute on the default node.
|
|
52
|
+
*
|
|
53
|
+
* @return {Node}
|
|
54
|
+
* @protected
|
|
55
|
+
* @override
|
|
56
|
+
*/
|
|
57
|
+
attachDefaultNode() {
|
|
58
|
+
const node = super.attachDefaultNode();
|
|
59
|
+
|
|
60
|
+
if (node) {
|
|
61
|
+
this.__updateNodeId(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Override to restore default node when a custom one is removed.
|
|
69
|
+
*
|
|
70
|
+
* @protected
|
|
71
|
+
*/
|
|
72
|
+
restoreDefaultNode() {
|
|
73
|
+
// To be implemented
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Override to update default node text on property change.
|
|
78
|
+
*
|
|
79
|
+
* @param {Node} node
|
|
80
|
+
* @protected
|
|
81
|
+
*/
|
|
82
|
+
updateDefaultNode(node) {
|
|
83
|
+
this.__notifyChange(node);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Setup the mutation observer on the node to update ID and notify host.
|
|
88
|
+
* Node doesn't get observed automatically until this method is called.
|
|
89
|
+
*
|
|
90
|
+
* @param {Node} node
|
|
91
|
+
* @protected
|
|
92
|
+
*/
|
|
93
|
+
observeNode(node) {
|
|
94
|
+
// Stop observing the previous node, if any.
|
|
95
|
+
if (this.__nodeObserver) {
|
|
96
|
+
this.__nodeObserver.disconnect();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.__nodeObserver = new MutationObserver((mutations) => {
|
|
100
|
+
mutations.forEach((mutation) => {
|
|
101
|
+
const target = mutation.target;
|
|
102
|
+
|
|
103
|
+
// Ensure the mutation target is the currently connected node
|
|
104
|
+
// to ignore async mutations dispatched for removed element.
|
|
105
|
+
const isCurrentNodeMutation = target === this.node;
|
|
106
|
+
|
|
107
|
+
if (mutation.type === 'attributes') {
|
|
108
|
+
// We use attributeFilter to only observe ID mutation,
|
|
109
|
+
// no need to check for attribute name separately.
|
|
110
|
+
if (isCurrentNodeMutation && target.id !== this.defaultId) {
|
|
111
|
+
this.__updateNodeId(target);
|
|
112
|
+
}
|
|
113
|
+
} else if (isCurrentNodeMutation || target.parentElement === this.node) {
|
|
114
|
+
// Node text content has changed.
|
|
115
|
+
this.__notifyChange(this.node);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Observe changes to node ID attribute, text content and children.
|
|
121
|
+
this.__nodeObserver.observe(node, {
|
|
122
|
+
attributes: true,
|
|
123
|
+
attributeFilter: ['id'],
|
|
124
|
+
childList: true,
|
|
125
|
+
subtree: true,
|
|
126
|
+
characterData: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns true if a node is an HTML element with children,
|
|
132
|
+
* or is a defined custom element, or has non-empty text.
|
|
133
|
+
*
|
|
134
|
+
* @param {Node} node
|
|
135
|
+
* @return {boolean}
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
__hasContent(node) {
|
|
139
|
+
if (!node) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
node.children.length > 0 ||
|
|
145
|
+
(node.nodeType === Node.ELEMENT_NODE && customElements.get(node.localName)) ||
|
|
146
|
+
(node.textContent && node.textContent.trim() !== '')
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Fire an event to notify the controller host about node changes.
|
|
152
|
+
*
|
|
153
|
+
* @param {Node} node
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
__notifyChange(node) {
|
|
157
|
+
this.dispatchEvent(
|
|
158
|
+
new CustomEvent('slot-content-changed', {
|
|
159
|
+
detail: { hasContent: this.__hasContent(node), node },
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Set default ID on the node in case it is an HTML element.
|
|
166
|
+
*
|
|
167
|
+
* @param {Node} node
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
__updateNodeId(node) {
|
|
171
|
+
if (node.nodeType === Node.ELEMENT_NODE && !node.id) {
|
|
172
|
+
node.id = this.defaultId;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|