@bootkit/ng0 0.0.0-alpha.24 → 0.0.0-alpha.25

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.
@@ -1,36 +1,132 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, Renderer2, DestroyRef, ChangeDetectorRef, signal, computed, ElementRef, input, booleanAttribute, EventEmitter, effect, TemplateRef, forwardRef, HostListener, Output, ContentChild, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
2
+ import { input, inject, ElementRef, ViewEncapsulation, ChangeDetectionStrategy, Component, ChangeDetectorRef, signal, booleanAttribute, EventEmitter, effect, untracked, computed, TemplateRef, forwardRef, HostListener, Output, ContentChild, ViewChildren, NgModule } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
5
  import { dataSourceAttribute, DataRequest } from '@bootkit/ng0/data';
6
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
6
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
8
- import { defaultEqualityComparer, equalityComparerAttribute, defaultValueWriter, valueWriterAttribute, noopFilter, filterPredicateAttribute, CssClassAttribute, sequentialIdGenerator } from '@bootkit/ng0/common';
9
7
  import { LocalizationService, defaultObjectFormatter, objectFormatterAttribute } from '@bootkit/ng0/localization';
8
+ import { defaultEqualityComparer, equalityComparerAttribute, defaultValueWriter, valueWriterAttribute, noopFilter, filterPredicateAttribute, trackByIndex, TrackByAttribute, CssClassAttribute, IdGeneratorAttribute, IfDirective } from '@bootkit/ng0/common';
10
9
 
11
10
  /**
12
- * Select component that allows users to choose an option from a dropdown list.
11
+ * ListItemComponent represents an individual item within a ListComponent.
12
+ */
13
+ class ListItemComponent {
14
+ /**
15
+ * The value associated with the item. This can be of any type.
16
+ */
17
+ value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : []));
18
+ /**
19
+ * The id of the item.
20
+ */
21
+ id = input(...(ngDevMode ? [undefined, { debugName: "id" }] : []));
22
+ /**
23
+ * Reference to the parent list component
24
+ */
25
+ list = inject(ListComponent);
26
+ /**
27
+ * Reference to the host element
28
+ */
29
+ elementRef = inject(ElementRef);
30
+ /**
31
+ * Indicates whether the item is active.
32
+ * @returns True if the item is active, false otherwise.
33
+ */
34
+ isActive() {
35
+ return this.list.isActive(this);
36
+ }
37
+ /**
38
+ * Indicates whether the item is selected.
39
+ * @returns True if the item is selected, false otherwise.
40
+ */
41
+ isSelected() {
42
+ return this.list.isSelected(this.value());
43
+ }
44
+ /**
45
+ * Selects the item.
46
+ * @returns
47
+ */
48
+ select() {
49
+ return this.list.select(this.value());
50
+ }
51
+ /**
52
+ * Deselects the item.
53
+ * @returns
54
+ */
55
+ deselect() {
56
+ this.list.deselect(this);
57
+ }
58
+ /**
59
+ * Toggles the selection state of the item.
60
+ * @returns void
61
+ */
62
+ toggle() {
63
+ this.list.toggle(this.value());
64
+ }
65
+ // /**
66
+ // * Indicates whether the item is disabled. Default is false.
67
+ // */
68
+ // public readonly disabled = input(false, { transform: booleanAttribute });
69
+ /**
70
+ * Scrolls the item into view within its parent container.
71
+ * @param position The vertical alignment of the item after scrolling.
72
+ * Can be 'start', 'center', 'end', or 'nearest'.
73
+ * Default is 'nearest'.
74
+ * @param behavior The scrolling behavior.
75
+ */
76
+ scrollIntoView(position, behavior) {
77
+ this.elementRef.nativeElement.scrollIntoView({ block: position, behavior: behavior });
78
+ }
79
+ /**
80
+ * Sets focus on the item.
81
+ */
82
+ focus() {
83
+ this.elementRef.nativeElement.focus();
84
+ }
85
+ _getTabIndex() {
86
+ let focus = this.list.focus();
87
+ // if (this.list.isDisabled()) {
88
+ // return undefined;
89
+ // }
90
+ if (focus == 'none' || focus == 'activeDescendant') {
91
+ return undefined;
92
+ }
93
+ else {
94
+ // focus: roving
95
+ return this.isActive() ? 0 : -1;
96
+ }
97
+ }
98
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
99
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: ListItemComponent, isStandalone: true, selector: "ng0-list-item", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.active": "isActive()", "class.selected": "isSelected()", "attr.id": "id()", "attr.tabIndex": "_getTabIndex()" } }, exportAs: ["ng0ListItem"], ngImport: i0, template: "@let formatter = list.formatBy();\r\n\r\n@if(list.itemTemplate) {\r\n<ng-container *ngTemplateOutlet=\"list.itemTemplate; context: { $implicit: {ref: this, value: value()}}\" />\r\n} @else {\r\n@if(list.showSelectionIndicator()) {\r\n<input class=\"form-check-input ng0-list-selection-indicator\"\r\n tabindex=\"-1\"\r\n [checked]=\"isSelected()\"\r\n [attr.type]=\"list.multiple() ? 'checkbox' : 'radio'\">\r\n}\r\n\r\n{{formatter(value())}}\r\n}", styles: ["ng0-list-item{display:flex;padding:.5rem}ng0-list-item .ng0-list-selection-indicator{margin-inline-end:.5rem}ng0-list-item.selected{background-color:var(--bs-primary);color:var(--bs-light)}ng0-list-item.active:not(.selected){background-color:color-mix(in srgb,var(--bs-primary),white 85%)}ng0-list-item:hover:not(.selected):not(.disabled):not(.active){background-color:color-mix(in srgb,var(--bs-primary),white 95%)}ng0-list-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}ng0-list-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}ng0-list-item:focus,ng0-list-item:focus-visible{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
100
+ }
101
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ListItemComponent, decorators: [{
102
+ type: Component,
103
+ args: [{ selector: 'ng0-list-item', exportAs: 'ng0ListItem', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
104
+ CommonModule,
105
+ ], host: {
106
+ '[class.active]': 'isActive()',
107
+ '[class.selected]': 'isSelected()',
108
+ '[attr.id]': 'id()',
109
+ '[attr.tabIndex]': '_getTabIndex()'
110
+ }, template: "@let formatter = list.formatBy();\r\n\r\n@if(list.itemTemplate) {\r\n<ng-container *ngTemplateOutlet=\"list.itemTemplate; context: { $implicit: {ref: this, value: value()}}\" />\r\n} @else {\r\n@if(list.showSelectionIndicator()) {\r\n<input class=\"form-check-input ng0-list-selection-indicator\"\r\n tabindex=\"-1\"\r\n [checked]=\"isSelected()\"\r\n [attr.type]=\"list.multiple() ? 'checkbox' : 'radio'\">\r\n}\r\n\r\n{{formatter(value())}}\r\n}", styles: ["ng0-list-item{display:flex;padding:.5rem}ng0-list-item .ng0-list-selection-indicator{margin-inline-end:.5rem}ng0-list-item.selected{background-color:var(--bs-primary);color:var(--bs-light)}ng0-list-item.active:not(.selected){background-color:color-mix(in srgb,var(--bs-primary),white 85%)}ng0-list-item:hover:not(.selected):not(.disabled):not(.active){background-color:color-mix(in srgb,var(--bs-primary),white 95%)}ng0-list-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}ng0-list-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}ng0-list-item:focus,ng0-list-item:focus-visible{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}\n"] }]
111
+ }] });
112
+
113
+ let uuid = 0;
114
+ /**
115
+ * ListComponent is a versatile component that displays a list of items with support for single or multiple selection,
116
+ * custom item templates, filtering, and keyboard navigation.
13
117
  */
14
118
  class ListComponent {
15
- _document = inject(DOCUMENT);
16
- _ls = inject(LocalizationService);
17
- _renderer = inject(Renderer2);
18
- _destroyRef = inject(DestroyRef);
119
+ _localizationService = inject(LocalizationService);
19
120
  _changeDetector = inject(ChangeDetectorRef);
20
- _value = undefined;
121
+ _value = signal(undefined, ...(ngDevMode ? [{ debugName: "_value" }] : []));
21
122
  _changeCallback;
22
123
  _touchCallback;
23
- _selectedIndices = new Set();
24
- _items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : []));
124
+ _sourceItems = signal([], ...(ngDevMode ? [{ debugName: "_sourceItems" }] : []));
125
+ _selectedValues = new Set();
126
+ _activeItem = signal(undefined, ...(ngDevMode ? [{ debugName: "_activeItem" }] : []));
127
+ _visibleItems;
25
128
  _isDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_isDisabled" }] : []));
26
- _activeOptionIndex = signal(-1, ...(ngDevMode ? [{ debugName: "_activeOptionIndex" }] : []));
27
- _itemTemplate;
28
- _ariaActiveDescendant = computed(() => {
29
- if (this.focus() == 'activeDescendant' && this._activeOptionIndex() > -1 && this._items().length) {
30
- return this._items()[this._activeOptionIndex()].id;
31
- }
32
- return undefined;
33
- }, ...(ngDevMode ? [{ debugName: "_ariaActiveDescendant" }] : []));
129
+ itemTemplate;
34
130
  /**
35
131
  * Reference to the host element
36
132
  */
@@ -66,8 +162,8 @@ class ListComponent {
66
162
  * Custom format function to convert an item to a string for display.
67
163
  * Default converts the item to a string using its toString method.
68
164
  */
69
- formatBy = input(defaultObjectFormatter, ...(ngDevMode ? [{ debugName: "formatBy", transform: objectFormatterAttribute(this._ls.get()) }] : [{
70
- transform: objectFormatterAttribute(this._ls.get())
165
+ formatBy = input(defaultObjectFormatter, ...(ngDevMode ? [{ debugName: "formatBy", transform: objectFormatterAttribute(this._localizationService.get()) }] : [{
166
+ transform: objectFormatterAttribute(this._localizationService.get())
71
167
  }]));
72
168
  /**
73
169
  * Custom value extractor function to extract the value of any object while writing values.
@@ -83,6 +179,23 @@ class ListComponent {
83
179
  filterBy = input(noopFilter, ...(ngDevMode ? [{ debugName: "filterBy", transform: filterPredicateAttribute }] : [{
84
180
  transform: filterPredicateAttribute
85
181
  }]));
182
+ /**
183
+ * A function that uniquely identifies each item in the list.
184
+ * If set to a function, it will be called with the index and item as arguments to generate the unique id.
185
+ * If set to a string, it will be used as the property name to extract the unique id from each item.
186
+ * Two predifined trackBy options are available:
187
+ * - '@index': uses the index of the item as its unique id.
188
+ * - '@item': uses the item itself as its unique id.
189
+ * @example
190
+ * trackBy="@index"
191
+ * trackBy="@item"
192
+ * trackBy="objectFieldName"
193
+ * [trackBy]="customTrackByFunction"
194
+ * @default trackByIndex
195
+ */
196
+ trackBy = input(trackByIndex, ...(ngDevMode ? [{ debugName: "trackBy", transform: TrackByAttribute }] : [{
197
+ transform: TrackByAttribute
198
+ }]));
86
199
  /**
87
200
  * CSS class or classes to apply to the list container.
88
201
  * Default is undefined.
@@ -99,188 +212,108 @@ class ListComponent {
99
212
  */
100
213
  focus = input('activeDescendant', ...(ngDevMode ? [{ debugName: "focus" }] : []));
101
214
  /**
102
- * Custom id generator function to generate unique ids for each item.
103
- * Default generates sequential ids with the prefix 'ng0-list-item-'.
104
- * If set to undefined, no ids will be generated.
105
- * @default sequentialIdGenerator('ng0-list-item-')
215
+ * A function that generates unique ids for each item in the list.
216
+ * If set to a function, it will be called with the item as an argument to generate the id.
217
+ * If set to undefined, no ids will be generated for the items.
218
+ * @default undefined
106
219
  */
107
- idGenerator = input(sequentialIdGenerator('ng0-list-item-'), ...(ngDevMode ? [{ debugName: "idGenerator" }] : []));
220
+ idGenerator = input(undefined, ...(ngDevMode ? [{ debugName: "idGenerator", transform: IdGeneratorAttribute }] : [{
221
+ transform: IdGeneratorAttribute
222
+ }]));
108
223
  /**
109
224
  * Event emitted when the selection state of an item changes by user interaction.
110
225
  */
111
226
  selectionChange = new EventEmitter();
227
+ _uuid;
112
228
  constructor() {
229
+ this._uuid = ++uuid;
113
230
  effect(() => {
114
- let source = this.source(); // track source
115
- this._activeOptionIndex.set(-1);
116
- this._selectedIndices.clear();
117
- this._loadItems();
231
+ let source = this.source();
232
+ source.load(new DataRequest()).subscribe(res => {
233
+ untracked(() => {
234
+ this._activeItem.set(undefined);
235
+ this._sourceItems.set(res.data);
236
+ this._findAndSelectItems();
237
+ });
238
+ });
118
239
  });
119
240
  }
120
241
  /**
121
- * Gets the items of the list component.
122
- * @returns A readonly array of the items in the list.
242
+ * Indicates whether an item is active.
243
+ * @param item
244
+ * @returns
123
245
  */
124
- items() {
125
- return [...this._items()];
246
+ isActive(item) {
247
+ return item === this._activeItem();
126
248
  }
127
249
  /**
128
- * Sets an option as active
129
- * @param index The index of the option to set as active.
130
- * @param scrollIntoView Whether to scroll the active option into view. Default is true.
131
- * @returns void
250
+ * Indicates whether the given value is selected.
251
+ * @param item
252
+ * @returns
132
253
  */
133
- active(index, scrollIntoView = true) {
134
- if (index < 0 || index >= this._items().length) {
135
- throw Error('Index out of range');
136
- }
137
- this._activeOptionIndex.set(index);
138
- if (scrollIntoView) {
139
- this.scrollIntoView(this._activeOptionIndex(), 'nearest');
140
- }
254
+ isSelected(value) {
255
+ return this._selectedValues.has(value);
141
256
  }
142
257
  /**
143
- * Selects an option by index
144
- * @param index The index of the option to select.
145
- * @returns void
258
+ * Selects the given value.
259
+ * @param item
146
260
  */
147
- select(index) {
148
- this._verifyIndexRange(index);
149
- if (this._selectedIndices.has(index)) {
150
- return;
151
- }
152
- else {
153
- if (!this.multiple()) {
154
- this._selectedIndices.clear();
155
- }
156
- this._selectedIndices.add(index);
157
- }
158
- if (this.multiple()) {
159
- this._value = [];
160
- for (const idx of this._selectedIndices) {
161
- this._value.push(this.writeBy()(this._items()[idx].value));
162
- }
163
- }
164
- else {
165
- this._value = this.writeBy()(this._items()[index].value);
261
+ select(value) {
262
+ if (!this._selectedValues.has(value())) {
263
+ this._selectedValues.add(value());
264
+ this._changeCallback?.(this._value());
166
265
  }
167
- this._changeCallback?.(this._value);
168
- this._changeDetector.markForCheck();
169
266
  }
170
267
  /**
171
- * Deselects an option by index
172
- * @param index The index of the option to deselect.
173
- * @returns void
268
+ * Deselects the given value.
269
+ * @param item
174
270
  */
175
- deselect(index) {
176
- this._verifyIndexRange(index);
177
- if (this._selectedIndices.has(index)) {
178
- this._selectedIndices.delete(index);
179
- }
180
- else {
181
- return;
182
- }
183
- if (this.multiple()) {
184
- this._value = [];
185
- for (const idx of this._selectedIndices) {
186
- this._value.push(this.writeBy()(this._items()[idx].value));
187
- }
188
- }
189
- else {
190
- this._value = undefined;
191
- }
192
- this._changeCallback?.(this._value);
193
- this._changeDetector.markForCheck();
271
+ deselect(value) {
272
+ this._selectedValues.delete(value);
273
+ this._changeCallback?.(this._value());
194
274
  }
195
275
  /**
196
- * Toggles the selection state of an option by index
197
- * @param index The index of the option to toggle.
198
- * @returns void
276
+ * Toggles the selection state of the given value.
277
+ * @param item
199
278
  */
200
- toggle(index) {
201
- if (this.isSelected(index)) {
202
- this.deselect(index);
279
+ toggle(value) {
280
+ if (this.isSelected(value)) {
281
+ this.deselect(value);
203
282
  }
204
283
  else {
205
- this.select(index);
284
+ this.select(value);
206
285
  }
207
286
  }
208
287
  /**
209
- * Checks if an option is selected.
210
- * @param index The index of the option to check.
211
- * @returns True if the option is selected, false otherwise.
288
+ * Deselects all items in the list.
212
289
  */
213
- isSelected(index) {
214
- return this._selectedIndices.has(index);
290
+ deselectAll() {
291
+ this._selectedValues.clear();
292
+ this._changeCallback?.(this._value());
215
293
  }
216
294
  /**
217
- * Checks if an option is active.
218
- * @param index The index of the option to check.
219
- * @returns True if the option is active, false otherwise.
295
+ * Selects all items in the list. Only applicable in multiple selection mode.
220
296
  */
221
- isActive(index) {
222
- return this._activeOptionIndex() === index;
223
- }
224
- /**
225
- * Sets the value of the list component.
226
- * @param value The value to set. Can be a single value or an array of values in multiple selection mode.
227
- */
228
- set(value) {
229
- this._setValue(value, true);
230
- }
231
- /**
232
- * Gets the currently selected indices.
233
- * @returns An array of the currently selected indices.
234
- * @description
235
- * - In single selection mode, the array will contain at most one item.
236
- * - In multiple selection mode, the array can contain multiple items.
237
- * - Changing the selection should be done using select(), deselect(), or toggle() methods to ensure proper event emission and state management.
238
- * - Direct manipulation of the returned array will not affect the component's state.
239
- */
240
- selectedIndices() {
241
- return Array.from(this._selectedIndices);
242
- }
243
- /**
244
- * Scrolls the item at the specified index into view within the dropdown list.
245
- * @param index The index of the item to scroll into view.
246
- * @param position The vertical alignment of the item after scrolling.
247
- * Can be 'start', 'center', 'end', or 'nearest'.
248
- * Default is 'nearest'.
249
- * @param behavior The scrolling behavior.
250
- */
251
- scrollIntoView(index, position, behavior) {
252
- let item = this._items()[index];
253
- let elm = this._document.getElementById(item.id);
254
- elm.scrollIntoView({ block: position, behavior: behavior });
297
+ selectAll() {
298
+ if (this.multiple()) {
299
+ this._selectedValues.clear();
300
+ this._sourceItems().forEach(i => this._selectedValues.add(i));
301
+ this._changeCallback?.(this._value());
302
+ }
303
+ throw new Error('selectAll is only available in multiple selection mode.');
255
304
  }
256
305
  writeValue(value) {
257
- this._setValue(value, false);
258
- }
259
- _setValue(value, fireCallback) {
260
- if (this.multiple() && value !== null && value !== undefined && !Array.isArray(value)) {
261
- throw Error('invalid value. Expected an array in multiple selection mode.');
262
- }
263
- let compareBy = this.compareBy();
264
- let findAndSelect = (v) => {
265
- let index = this._items().findIndex(i => compareBy(i.value, v));
266
- if (index > -1) {
267
- this._selectedIndices.add(index);
268
- }
269
- };
270
- this._selectedIndices.clear();
271
306
  if (this.multiple()) {
272
- if (Array.isArray(value)) {
273
- value.forEach(i => findAndSelect(i));
307
+ if (value === null || value === undefined) {
308
+ value = [];
309
+ }
310
+ else if (!Array.isArray(value)) {
311
+ throw Error('invalid value. Expected an array in multiple selection mode.');
274
312
  }
275
313
  }
276
- else {
277
- findAndSelect(value);
278
- }
279
- this._value = value;
314
+ this._value.set(value);
315
+ this._findAndSelectItems();
280
316
  this._changeDetector.markForCheck();
281
- if (fireCallback) {
282
- this._changeCallback?.(value);
283
- }
284
317
  }
285
318
  registerOnChange(fn) {
286
319
  this._changeCallback = fn;
@@ -291,150 +324,180 @@ class ListComponent {
291
324
  setDisabledState(isDisabled) {
292
325
  this._isDisabled.set(isDisabled);
293
326
  }
294
- _getItemTabIndex(index) {
295
- let focus = this.focus();
296
- if (this._isDisabled() || focus == 'none' || focus == 'activeDescendant') {
297
- return undefined;
327
+ _handleUserSelection(item) {
328
+ let value = item.value();
329
+ this._activeItem.set(item);
330
+ if (this.multiple()) {
331
+ if (this.isSelected(value)) {
332
+ this._selectedValues.delete(value);
333
+ }
334
+ else {
335
+ this._selectedValues.add(value);
336
+ }
298
337
  }
299
338
  else {
300
- // roving
301
- return this._activeOptionIndex() === index ? 0 : -1;
339
+ this._selectedValues.clear();
340
+ this._selectedValues.add(value);
302
341
  }
342
+ this._updateValue();
343
+ this._changeCallback?.(this._value());
344
+ this.selectionChange.emit({
345
+ item: item,
346
+ list: this,
347
+ });
348
+ this._changeDetector.detectChanges();
303
349
  }
304
- _handleUserSelection(index, item) {
305
- let selected;
306
- this.active(index);
307
- if (this.multiple() && this.isSelected(index)) {
308
- this.deselect(index);
309
- selected = false;
350
+ _showLoadingSppiner = computed(() => {
351
+ let source = this.source();
352
+ return source.isLoading() && source.type == 'remote';
353
+ }, ...(ngDevMode ? [{ debugName: "_showLoadingSppiner" }] : []));
354
+ _findAndSelectItems() {
355
+ let value = this._value();
356
+ let compareBy = this.compareBy();
357
+ let findAndSelect = (v) => {
358
+ let index = this._sourceItems().findIndex(i => compareBy(i, v));
359
+ if (index > -1) {
360
+ let item = this._sourceItems().at(index);
361
+ this._selectedValues.add(item);
362
+ }
363
+ };
364
+ if (this.multiple()) {
365
+ if (Array.isArray(value)) {
366
+ value.forEach(v => findAndSelect(v));
367
+ }
310
368
  }
311
369
  else {
312
- this.select(index);
313
- selected = true;
370
+ findAndSelect(value);
314
371
  }
315
- this.selectionChange.emit({
316
- value: item.value,
317
- index: index,
318
- selected: selected,
319
- selectedIndices: this.selectedIndices(),
320
- list: this,
321
- });
372
+ this._changeDetector.markForCheck();
322
373
  }
323
- _loadItems() {
324
- var r = new DataRequest();
325
- this.source().load(r).pipe(takeUntilDestroyed(this._destroyRef)).subscribe(res => {
326
- let items = this._createItems(res.data);
327
- this._items().push(...items);
328
- });
329
- // listen to changes
330
- this.source().change.subscribe(e => {
331
- let items = this._items();
332
- e.changes.forEach(change => {
333
- switch (change.type) {
334
- case 'push':
335
- this._items().push(...this._createItems(change.items));
336
- break;
337
- // case 'replace':
338
- // change.replacements.forEach(({ index, value }) => {
339
- // items[index] = { id: this.idGenerator()(value), value };
340
- // });
341
- // break;
342
- // case 'remove':
343
- // this._activeOptionIndex.set(-1);
344
- // change.indices.forEach(i => {
345
- // this.deselect(i);
346
- // items.splice(i, 1)
347
- // });
348
- // break;
349
- }
350
- this._changeDetector.markForCheck();
374
+ _updateValue() {
375
+ if (this.multiple()) {
376
+ let values = [];
377
+ this._selectedValues.forEach(v => {
378
+ values.push(this.writeBy()(v));
351
379
  });
352
- });
353
- }
354
- _createItems(items) {
355
- let idGenerator = this.idGenerator();
356
- return items.map(x => ({
357
- id: idGenerator(x),
358
- value: x,
359
- }));
380
+ this._value.set(values);
381
+ }
382
+ else {
383
+ if (this._selectedValues.size > 0) {
384
+ let first = this._selectedValues.values().next().value;
385
+ this._value.set(this.writeBy()(first));
386
+ }
387
+ else {
388
+ this._value.set(undefined);
389
+ }
390
+ }
360
391
  }
361
- _onHostClick() {
362
- if (this.focus() != 'none') {
363
- this.elementRef.nativeElement.focus();
392
+ _hostAriaActiveDescendant = computed(() => {
393
+ if (this._activeItem() && !this._isDisabled() && this.focus() == 'activeDescendant') {
394
+ return this._activeItem().id();
395
+ }
396
+ return undefined;
397
+ }, ...(ngDevMode ? [{ debugName: "_hostAriaActiveDescendant" }] : []));
398
+ _hostTabIndex = computed(() => {
399
+ let isDisabled = this._isDisabled(); // track _isDisabled
400
+ let activeItem = this._activeItem(); // track _activeItem
401
+ if (isDisabled) {
402
+ return undefined;
403
+ }
404
+ switch (this.focus()) {
405
+ case 'none':
406
+ return undefined;
407
+ case 'activeDescendant':
408
+ return 0;
409
+ case 'roving':
410
+ return activeItem ? undefined : 0;
364
411
  }
412
+ }, ...(ngDevMode ? [{ debugName: "_hostTabIndex" }] : []));
413
+ _onHostClick() {
414
+ // if (this.focus() != 'none') {
415
+ // this.elementRef.nativeElement.focus();
416
+ // }
365
417
  }
366
- _onKeydown(e, firedByFilter = false) {
418
+ _onHostBlur() {
419
+ this._touchCallback?.();
420
+ }
421
+ _onKeydown(e) {
367
422
  if (this._isDisabled())
368
423
  return;
369
- let optionsCount = this._items().length;
370
- if (optionsCount == 0) {
424
+ let visibleItemsCount = this._visibleItems.length;
425
+ if (visibleItemsCount == 0) {
371
426
  return;
372
427
  }
373
- let index = this._activeOptionIndex();
428
+ let activeItemindex = this._visibleItems.toArray().findIndex(i => i === this._activeItem());
429
+ console.log(this._activeItem(), activeItemindex);
374
430
  switch (e.key) {
375
431
  case 'ArrowDown':
376
- if (index < optionsCount - 1) {
377
- this.active(index + 1);
432
+ if (activeItemindex == -1) {
433
+ const first = this._visibleItems.get(0);
434
+ this._activeItem.set(first);
435
+ }
436
+ else if (activeItemindex < visibleItemsCount - 1) {
437
+ const next = this._visibleItems.get(activeItemindex + 1);
438
+ this._activeItem.set(next);
378
439
  }
379
440
  e.preventDefault();
380
441
  break;
381
442
  case 'ArrowUp':
382
- if (index > 0) {
383
- this.active(index - 1);
443
+ if (activeItemindex == -1) {
444
+ const last = this._visibleItems.get(visibleItemsCount - 1);
445
+ this._activeItem.set(last);
446
+ }
447
+ else if (activeItemindex > 0) {
448
+ const previous = this._visibleItems.get(activeItemindex - 1);
449
+ this._activeItem.set(previous);
384
450
  }
385
451
  e.preventDefault();
386
452
  break;
387
- case 'Tab': // Go to next item if roving focus is enabled
388
- // if (this.focus() === 'roving' && index < optionsCount - 1) {
389
- // this.active(index + 1);
390
- // e.preventDefault();
391
- // }
392
- break;
393
453
  case 'Enter':
394
- if (index > -1) {
395
- this._handleUserSelection(index, this._items()[index]);
454
+ if (activeItemindex > -1) {
455
+ this._handleUserSelection(this._visibleItems.get(activeItemindex));
396
456
  }
397
- // e.preventDefault();
398
457
  break;
399
458
  case 'Home':
400
- this.active(0);
459
+ const first = this._visibleItems.get(0);
460
+ this._activeItem.set(first);
401
461
  e.preventDefault();
402
462
  break;
403
463
  case 'End':
404
- this.active(optionsCount - 1);
464
+ const last = this._visibleItems.get(visibleItemsCount - 1);
465
+ this._activeItem.set(last);
405
466
  e.preventDefault();
406
467
  break;
407
468
  }
408
- }
409
- _verifyIndexRange(index) {
410
- let optionsCount = this._items().length;
411
- if (optionsCount == 0 || index < 0 || index > optionsCount - 1) {
412
- throw new Error('Index out of range');
469
+ if (this.focus() === 'roving') {
470
+ this._activeItem()?.focus();
413
471
  }
414
472
  }
415
473
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
416
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: ListComponent, isStandalone: true, selector: "ng0-list", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: true, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, showSelectionIndicator: { classPropertyName: "showSelectionIndicator", publicName: "showSelectionIndicator", isSignal: true, isRequired: false, transformFunction: null }, compareBy: { classPropertyName: "compareBy", publicName: "compareBy", isSignal: true, isRequired: false, transformFunction: null }, formatBy: { classPropertyName: "formatBy", publicName: "formatBy", isSignal: true, isRequired: false, transformFunction: null }, writeBy: { classPropertyName: "writeBy", publicName: "writeBy", isSignal: true, isRequired: false, transformFunction: null }, filterBy: { classPropertyName: "filterBy", publicName: "filterBy", isSignal: true, isRequired: false, transformFunction: null }, itemClass: { classPropertyName: "itemClass", publicName: "itemClass", isSignal: true, isRequired: false, transformFunction: null }, focus: { classPropertyName: "focus", publicName: "focus", isSignal: true, isRequired: false, transformFunction: null }, idGenerator: { classPropertyName: "idGenerator", publicName: "idGenerator", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { listeners: { "click": "_onHostClick()", "keydown": "_onKeydown($event)" }, properties: { "class.ng0-list-loading": "source().isLoading()", "attr.aria-activedescendant": "_ariaActiveDescendant()", "attr.disabled": "_isDisabled()", "attr.tabindex": "_isDisabled() || focus() === \"none\" ? undefined : \"0\"", "attr.aria-disabled": "_isDisabled()" } }, providers: [{
474
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: ListComponent, isStandalone: true, selector: "ng0-list", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: true, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, showSelectionIndicator: { classPropertyName: "showSelectionIndicator", publicName: "showSelectionIndicator", isSignal: true, isRequired: false, transformFunction: null }, compareBy: { classPropertyName: "compareBy", publicName: "compareBy", isSignal: true, isRequired: false, transformFunction: null }, formatBy: { classPropertyName: "formatBy", publicName: "formatBy", isSignal: true, isRequired: false, transformFunction: null }, writeBy: { classPropertyName: "writeBy", publicName: "writeBy", isSignal: true, isRequired: false, transformFunction: null }, filterBy: { classPropertyName: "filterBy", publicName: "filterBy", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, itemClass: { classPropertyName: "itemClass", publicName: "itemClass", isSignal: true, isRequired: false, transformFunction: null }, focus: { classPropertyName: "focus", publicName: "focus", isSignal: true, isRequired: false, transformFunction: null }, idGenerator: { classPropertyName: "idGenerator", publicName: "idGenerator", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { listeners: { "click": "_onHostClick()", "blur": "_onHostBlur()", "keydown": "_onKeydown($event)" }, properties: { "class.ng0-list-loading": "source().isLoading()", "attr.aria-activedescendant": "_hostAriaActiveDescendant()", "attr.disabled": "_isDisabled() ? \"\" : undefined", "attr.aria-disabled": "_isDisabled() ? \"\" : undefined", "attr.tabindex": "_hostTabIndex()" } }, providers: [{
417
475
  provide: NG_VALUE_ACCESSOR,
418
476
  useExisting: forwardRef(() => ListComponent),
419
477
  multi: true
420
- }], queries: [{ propertyName: "_itemTemplate", first: true, predicate: TemplateRef, descendants: true }], exportAs: ["ng0List"], ngImport: i0, template: "@let formatter = formatBy();\r\n@let filter = filterBy();\r\n\r\n\r\n@for (item of _items(); track item.id) {\r\n@let selected= isSelected($index);\r\n@let active= isActive($index);\r\n\r\n@if(filter(item)) {\r\n<div\r\n [ngClass]=\"itemClass()(item)\"\r\n [attr.id]=\"item.id\"\r\n [class.active]=\"active\"\r\n [class.selected]=\"selected\"\r\n [attr.tabIndex]=\"_getItemTabIndex($index)\"\r\n (click)=\"_handleUserSelection($index, item)\">\r\n\r\n @if(_itemTemplate) {\r\n <ng-container *ngTemplateOutlet=\"_itemTemplate; context: { $implicit: {\r\n id: item.id,\r\n value: item.value,\r\n selected: selected, \r\n active: active,\r\n }}\" />\r\n } @else {\r\n\r\n @if(showSelectionIndicator()) {\r\n <input class=\"form-check-input ng0-list-selection-indicator\"\r\n tabindex=\"-1\"\r\n [checked]=\"selected\"\r\n [attr.type]=\"multiple() ? 'checkbox' : 'radio'\"\r\n [attr.name]=\"item.id\"\r\n [attr.id]=\"item.id\">\r\n }\r\n\r\n {{formatter(item.value)}}\r\n\r\n }\r\n</div>\r\n}\r\n}", styles: [":host{display:block;overflow-y:auto;overflow-x:hidden;-webkit-user-select:none;user-select:none;border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius)}:host.disabled{background-color:var(--bs-secondary-bg)}:host .ng0-list-item{display:flex;padding:.5rem}:host .ng0-list-item .ng0-list-selection-indicator{margin-inline-end:.5rem}:host .ng0-list-item.selected{background-color:var(--bs-primary);color:var(--bs-light)}:host .ng0-list-item.active:not(.selected){background-color:var(--bs-gray-300)}:host .ng0-list-item:hover:not(.selected):not(.disabled):not(.active){background-color:var(--bs-gray-100)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
478
+ }], queries: [{ propertyName: "itemTemplate", first: true, predicate: TemplateRef, descendants: true }], viewQueries: [{ propertyName: "_visibleItems", predicate: ListItemComponent, descendants: true }], exportAs: ["ng0List"], ngImport: i0, template: "@let filter = filterBy();\r\n\r\n@for (item of _sourceItems(); track trackBy()($index, item)) {\r\n<ng0-list-item *ng0If=\"filter(item)\"\r\n #listItem=\"ng0ListItem\"\r\n [id]=\"idGenerator()?.(item)\"\r\n [value]=\"item\"\r\n [ngClass]=\"itemClass()(item)\"\r\n (click)=\"_handleUserSelection(listItem);\">\r\n</ng0-list-item>\r\n}\r\n\r\n@if(_showLoadingSppiner()) {\r\n@if(_sourceItems().length == 0) {\r\n<div style=\"text-align: center; padding: 0.5rem 0;\">\r\n <ng-container *ngTemplateOutlet=\"spinner\"></ng-container>\r\n</div>\r\n}@else {\r\n<div class=\"ng0-list-loading-cover\">\r\n <ng-container *ngTemplateOutlet=\"spinner\"></ng-container>\r\n</div>\r\n}\r\n}\r\n\r\n<ng-template #spinner>\r\n <div class=\"spinner-border spinner-sm text-primary ng0-list-loading-indicator\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n</ng-template>", styles: ["ng0-list{display:block;position:relative;-webkit-user-select:none;user-select:none;border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius);min-height:1rem}ng0-list:focus,ng0-list:focus-visible{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}ng0-list[disabled],ng0-list.disabled{opacity:.5;pointer-events:none}ng0-list .ng0-list-loading-cover{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffffb3;display:flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: IfDirective, selector: "[ng0If]", inputs: ["ng0If"], exportAs: ["ng0If"] }, { kind: "component", type: ListItemComponent, selector: "ng0-list-item", inputs: ["value", "id"], exportAs: ["ng0ListItem"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
421
479
  }
422
480
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ListComponent, decorators: [{
423
481
  type: Component,
424
- args: [{ selector: 'ng0-list', exportAs: 'ng0List', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
482
+ args: [{ selector: 'ng0-list', exportAs: 'ng0List', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
425
483
  CommonModule,
484
+ IfDirective,
485
+ ListItemComponent
426
486
  ], providers: [{
427
487
  provide: NG_VALUE_ACCESSOR,
428
488
  useExisting: forwardRef(() => ListComponent),
429
489
  multi: true
430
490
  }], host: {
431
491
  '[class.ng0-list-loading]': 'source().isLoading()',
432
- '[attr.aria-activedescendant]': '_ariaActiveDescendant()',
433
- '[attr.disabled]': '_isDisabled()',
434
- '[attr.tabindex]': '_isDisabled() || focus() === "none" ? undefined : "0"',
435
- '[attr.aria-disabled]': '_isDisabled()'
436
- }, template: "@let formatter = formatBy();\r\n@let filter = filterBy();\r\n\r\n\r\n@for (item of _items(); track item.id) {\r\n@let selected= isSelected($index);\r\n@let active= isActive($index);\r\n\r\n@if(filter(item)) {\r\n<div\r\n [ngClass]=\"itemClass()(item)\"\r\n [attr.id]=\"item.id\"\r\n [class.active]=\"active\"\r\n [class.selected]=\"selected\"\r\n [attr.tabIndex]=\"_getItemTabIndex($index)\"\r\n (click)=\"_handleUserSelection($index, item)\">\r\n\r\n @if(_itemTemplate) {\r\n <ng-container *ngTemplateOutlet=\"_itemTemplate; context: { $implicit: {\r\n id: item.id,\r\n value: item.value,\r\n selected: selected, \r\n active: active,\r\n }}\" />\r\n } @else {\r\n\r\n @if(showSelectionIndicator()) {\r\n <input class=\"form-check-input ng0-list-selection-indicator\"\r\n tabindex=\"-1\"\r\n [checked]=\"selected\"\r\n [attr.type]=\"multiple() ? 'checkbox' : 'radio'\"\r\n [attr.name]=\"item.id\"\r\n [attr.id]=\"item.id\">\r\n }\r\n\r\n {{formatter(item.value)}}\r\n\r\n }\r\n</div>\r\n}\r\n}", styles: [":host{display:block;overflow-y:auto;overflow-x:hidden;-webkit-user-select:none;user-select:none;border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius)}:host.disabled{background-color:var(--bs-secondary-bg)}:host .ng0-list-item{display:flex;padding:.5rem}:host .ng0-list-item .ng0-list-selection-indicator{margin-inline-end:.5rem}:host .ng0-list-item.selected{background-color:var(--bs-primary);color:var(--bs-light)}:host .ng0-list-item.active:not(.selected){background-color:var(--bs-gray-300)}:host .ng0-list-item:hover:not(.selected):not(.disabled):not(.active){background-color:var(--bs-gray-100)}\n"] }]
437
- }], ctorParameters: () => [], propDecorators: { _itemTemplate: [{
492
+ '[attr.aria-activedescendant]': '_hostAriaActiveDescendant()',
493
+ '[attr.disabled]': '_isDisabled() ? "" : undefined',
494
+ '[attr.aria-disabled]': '_isDisabled() ? "" : undefined',
495
+ '[attr.tabindex]': '_hostTabIndex()',
496
+ }, template: "@let filter = filterBy();\r\n\r\n@for (item of _sourceItems(); track trackBy()($index, item)) {\r\n<ng0-list-item *ng0If=\"filter(item)\"\r\n #listItem=\"ng0ListItem\"\r\n [id]=\"idGenerator()?.(item)\"\r\n [value]=\"item\"\r\n [ngClass]=\"itemClass()(item)\"\r\n (click)=\"_handleUserSelection(listItem);\">\r\n</ng0-list-item>\r\n}\r\n\r\n@if(_showLoadingSppiner()) {\r\n@if(_sourceItems().length == 0) {\r\n<div style=\"text-align: center; padding: 0.5rem 0;\">\r\n <ng-container *ngTemplateOutlet=\"spinner\"></ng-container>\r\n</div>\r\n}@else {\r\n<div class=\"ng0-list-loading-cover\">\r\n <ng-container *ngTemplateOutlet=\"spinner\"></ng-container>\r\n</div>\r\n}\r\n}\r\n\r\n<ng-template #spinner>\r\n <div class=\"spinner-border spinner-sm text-primary ng0-list-loading-indicator\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n</ng-template>", styles: ["ng0-list{display:block;position:relative;-webkit-user-select:none;user-select:none;border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius);min-height:1rem}ng0-list:focus,ng0-list:focus-visible{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}ng0-list[disabled],ng0-list.disabled{opacity:.5;pointer-events:none}ng0-list .ng0-list-loading-cover{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffffb3;display:flex;align-items:center;justify-content:center}\n"] }]
497
+ }], ctorParameters: () => [], propDecorators: { _visibleItems: [{
498
+ type: ViewChildren,
499
+ args: [ListItemComponent]
500
+ }], itemTemplate: [{
438
501
  type: ContentChild,
439
502
  args: [TemplateRef]
440
503
  }], selectionChange: [{
@@ -442,6 +505,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
442
505
  }], _onHostClick: [{
443
506
  type: HostListener,
444
507
  args: ['click']
508
+ }], _onHostBlur: [{
509
+ type: HostListener,
510
+ args: ['blur']
445
511
  }], _onKeydown: [{
446
512
  type: HostListener,
447
513
  args: ['keydown', ['$event']]