• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.78
/src/list/selection/selection-list.ts
1
import { ScrollToService } from 'ngx-tethys/core';
2
import { IThyListOptionParentComponent, THY_LIST_OPTION_PARENT_COMPONENT, ThyListLayout, ThyListOption } from 'ngx-tethys/shared';
3
import { dom, helpers, keycodes } from 'ngx-tethys/util';
4
import { Subscription } from 'rxjs';
5
import { startWith } from 'rxjs/operators';
6
import { useHostRenderer } from '@tethys/cdk/dom';
7
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
8
import { SelectionModel } from '@angular/cdk/collections';
9
import {
10
    AfterContentInit,
11
    booleanAttribute,
12
    ChangeDetectionStrategy,
1✔
13
    ChangeDetectorRef,
14
    Component,
15
    ContentChildren,
16
    ElementRef,
17
    EventEmitter,
18
    forwardRef,
19
    HostBinding,
1✔
20
    Input,
21
    NgZone,
38✔
22
    OnDestroy,
38✔
23
    OnInit,
38✔
24
    Output,
11✔
25
    QueryList,
26
    Renderer2
27
} from '@angular/core';
28
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
20✔
29

20✔
30
import { ThySelectionListChange } from './selection.interface';
31

32
export type ThyListSize = 'sm' | 'md' | 'lg';
29✔
33

34
const listSizesMap = {
35
    sm: 'thy-list-sm'
20✔
36
};
37

38
/**
28✔
39
 * @name thy-selection-list,[thy-selection-list]
40
 * @order 20
41
 */
8✔
42
@Component({
43
    selector: 'thy-selection-list,[thy-selection-list]',
44
    template: '<ng-content></ng-content>',
45
    providers: [
46
        {
47
            provide: THY_LIST_OPTION_PARENT_COMPONENT,
48
            useExisting: ThySelectionList
49
        },
50
        {
10!
51
            provide: NG_VALUE_ACCESSOR,
10✔
52
            useExisting: forwardRef(() => ThySelectionList),
10✔
53
            multi: true
1✔
54
        }
1✔
55
    ],
1✔
56
    changeDetection: ChangeDetectionStrategy.OnPush,
57
    standalone: true
1!
58
})
1✔
59
export class ThySelectionList implements OnInit, OnDestroy, AfterContentInit, IThyListOptionParentComponent, ControlValueAccessor {
60
    private _keyManager: ActiveDescendantKeyManager<ThyListOption>;
UNCOV
61

×
62
    private _selectionChangesUnsubscribe$ = Subscription.EMPTY;
×
63

64
    private _bindKeyEventUnsubscribe: () => void;
65

66
    private _modelValues: any[];
67

10✔
68
    private hostRenderer = useHostRenderer();
10✔
69

10✔
70
    /** The currently selected options. */
3✔
71
    selectionModel: SelectionModel<any>;
72

10✔
73
    disabled: boolean;
74

75
    layout: ThyListLayout = 'list';
76

4!
77
    @HostBinding(`class.thy-list`) _isList = true;
4✔
78

4✔
79
    @HostBinding(`class.thy-selection-list`) _isSelectionList = true;
80

81
    @HostBinding(`class.thy-multiple-selection-list`) multiple = true;
82

83
    @HostBinding(`class.thy-grid-list`) isLayoutGrid = false;
38✔
84

85
    /**
86
     * @internal
87
     */
88
    @ContentChildren(ThyListOption, { descendants: true }) options: QueryList<ThyListOption>;
13✔
89

90
    /**
91
     * 改变 grid item 的选择模式,使其支持多选
49✔
92
     * @default true
93
     */
94
    @Input({ transform: booleanAttribute })
38✔
95
    set thyMultiple(value: boolean) {
96
        const previousValue = this.multiple;
97
        this.multiple = value;
3!
UNCOV
98
        if (previousValue !== this.multiple) {
×
99
            this._instanceSelectionModel();
×
100
        }
101
    }
3!
UNCOV
102

×
103
    /**
104
     * 绑定键盘事件的容器
105
     * @type HTMLElement | ElementRef | string
3✔
106
     * @default thy-selection-list 组件绑定的元素
107
     */
108
    @Input() thyBindKeyEventContainer: HTMLElement | ElementRef | string;
109

511✔
110
    /**
507✔
111
     * 出现滚动条的容器
112
     * @type HTMLElement | ElementRef | string
113
     * @default thy-selection-list 组件绑定的元素
4✔
114
     */
115
    @Input() thyScrollContainer: HTMLElement | ElementRef | string;
116

117
    /**
22✔
118
     * 键盘事件触发 Before 调用,如果返回 false 则停止继续执行
22✔
119
     */
3✔
120
    @Input() thyBeforeKeydown: (event?: KeyboardEvent) => boolean;
1✔
121

122
    /**
123
     * Option Value 唯一的 Key,用于存储哪些选择被选中的唯一值,只有 Option 的 thyValue 是对象的时才可以传入该选项
2✔
124
     */
125
    @Input() thyUniqueKey: string;
126

127
    /**
128
     * 比较2个选项的 Value 是否相同
129
     */
130
    @Input() thyCompareWith: (o1: any, o2: any) => boolean;
2✔
131

2✔
132
    /**
8✔
133
     * grid item 的展示样式
8!
134
     * @type list | grid
8✔
135
     * @default list
8✔
136
     */
137
    @Input() set thyLayout(value: ThyListLayout) {
138
        this.layout = value;
2!
139
        this.isLayoutGrid = value === 'grid';
2✔
140
    }
141

142
    /**
143
     * 是否自动激活第一项
1✔
144
     */
3✔
145
    @Input({ transform: booleanAttribute }) set thyAutoActiveFirstItem(value: boolean) {
146
        this.autoActiveFirstItem = value;
147
    }
148

1!
149
    /**
1✔
150
     * 改变 grid item 的大小,支持默认以及 sm 两种大小
151
     * @type sm | md | lg
UNCOV
152
     */
×
153
    @Input() set thySize(value: ThyListSize) {
154
        this._setListSize(value);
155
    }
156

20✔
157
    private spaceEnabled = true;
20!
158

20✔
159
    /**
160
     * 是否按下空格切换聚焦选项
161
     */
20✔
162
    @Input({ transform: booleanAttribute }) set thySpaceKeyEnabled(value: boolean) {
1✔
163
        this.spaceEnabled = value;
164
    }
165

166
    /**
39✔
167
     * 每当选项的选定状态发生更改时,都会触发更改事件
39✔
168
     * @type EventEmitter<ThySelectionListChange>
39✔
169
     */
39✔
170
    @Output() readonly thySelectionChange: EventEmitter<ThySelectionListChange> = new EventEmitter<ThySelectionListChange>();
39✔
171

39✔
172
    private autoActiveFirstItem: boolean;
39✔
173

39✔
174
    private _onTouched: () => void = () => {};
39✔
175

39✔
176
    private _onChange: (value: any) => void = (_: any) => {};
39✔
177

39✔
178
    private _emitChangeEvent(option: ThyListOption, event: Event) {
39✔
179
        this.thySelectionChange.emit({
39✔
180
            source: this,
39✔
181
            value: option.thyValue,
182
            option: option,
183
            event: event,
38✔
184
            selected: this.isSelected(option)
38✔
185
        });
38✔
186
    }
187

38✔
188
    private _emitModelValueChange() {
189
        if (this.options) {
190
            let selectedValues = this.selectionModel.selected;
42✔
191
            if (this.thyUniqueKey) {
21!
UNCOV
192
                selectedValues = selectedValues.map(selectedValue => {
×
193
                    const selectedOption = this.options.find(option => {
194
                        return option.thyValue[this.thyUniqueKey] === selectedValue;
21!
UNCOV
195
                    });
×
196
                    if (selectedOption) {
197
                        return selectedOption.thyValue;
198
                    } else {
42!
199
                        return this._modelValues.find(value => {
42✔
200
                            return value[this.thyUniqueKey] === selectedValue;
42✔
201
                        });
22✔
202
                    }
203
                });
42✔
204
            }
205
            this._modelValues = selectedValues;
206
            let changeValue = selectedValues;
20✔
207
            if (!this.multiple && selectedValues && selectedValues.length > 0) {
208
                changeValue = selectedValues[0];
209
            }
20✔
210
            this._onChange(changeValue);
211
        }
212
    }
20✔
213

214
    private _toggleFocusedOption(event: KeyboardEvent): void {
215
        if (this._keyManager.activeItem) {
8✔
216
            this.ngZone.run(() => {
217
                this.toggleOption(this._keyManager.activeItem, event);
6✔
218
            });
6✔
219
        }
1✔
220
    }
221

222
    private _initializeFocusKeyManager() {
7!
223
        this._keyManager = new ActiveDescendantKeyManager<ThyListOption>(this.options)
7✔
224
            .withWrap()
7✔
225
            // .withTypeAhead()
7✔
226
            // Allow disabled items to be focusable. For accessibility reasons, there must be a way for
227
            // screenreader users, that allows reading the different options of the list.
228
            .skipPredicate(() => false);
5✔
229
    }
1✔
230

231
    private _instanceSelectionModel() {
4✔
232
        this.selectionModel = new SelectionModel<any>(this.multiple);
233
    }
4✔
234

4✔
235
    private _getElementBySelector(element: HTMLElement | ElementRef | string): HTMLElement {
236
        return dom.getHTMLElementBySelector(element, this.elementRef);
2✔
237
    }
238

6!
239
    private _compareValue(value1: any, value2: any) {
240
        if (this.thyCompareWith) {
UNCOV
241
            const compareFn = this.thyCompareWith as (o1: any, o2: any) => boolean;
×
242
            return compareFn(value1, value2);
243
        } else if (this.thyUniqueKey) {
244
            return value1 && value1[this.thyUniqueKey] === value2 && value2[this.thyUniqueKey];
245
        } else {
8!
246
            return value1 === value2;
8✔
247
        }
248
    }
249

8✔
250
    private _getOptionSelectionValue(option: ThyListOption) {
8✔
251
        if (option.thyValue) {
252
            return this.thyUniqueKey ? option.thyValue[this.thyUniqueKey] : option.thyValue;
253
        } else {
254
            return option;
6✔
255
        }
256
    }
257

13✔
258
    private _setSelectionByValues(values: any[]) {
13✔
259
        this.selectionModel.clear();
260
        values.forEach(value => {
261
            if (this.thyUniqueKey) {
503✔
262
                this.selectionModel.select(value[this.thyUniqueKey]);
263
            } else {
264
                this.selectionModel.select(value);
1!
265
            }
1✔
266
        });
267
    }
268

269
    private _setAllOptionsSelected(toIsSelected: boolean) {
1!
270
        // Keep track of whether anything changed, because we only want to
1✔
271
        // emit the changed event when something actually changed.
272
        let hasChanged = false;
273

274
        this.options.forEach(option => {
275
            const fromIsSelected = this.selectionModel.isSelected(option.thyValue);
1✔
276
            if (fromIsSelected !== toIsSelected) {
277
                hasChanged = true;
278
                this.selectionModel.toggle(option.thyValue);
279
            }
1✔
280
        });
281

282
        if (hasChanged) {
38✔
283
            this._emitModelValueChange();
38✔
284
        }
39✔
285
    }
11!
286

11✔
287
    private _getOptionByValue(value: any) {
288
        return this.options.find(option => {
289
            return this._compareValue(option.thyValue, value);
290
        });
291
    }
292

39✔
293
    private _getActiveOption() {
39✔
294
        if (this._keyManager.activeItem) {
38✔
295
            return this._getOptionByValue(this._keyManager.activeItem.thyValue);
296
        } else {
297
            return null;
1✔
298
        }
299
    }
300

301
    private _setListSize(size: ThyListSize) {
302
        for (const key in listSizesMap) {
303
            if (listSizesMap.hasOwnProperty(key)) {
1✔
304
                this.hostRenderer.removeClass(listSizesMap[key]);
305
            }
306
        }
307
        if (size) {
308
            this.hostRenderer.addClass(listSizesMap[size]);
309
        }
310
    }
311

312
    constructor(
313
        private renderer: Renderer2,
314
        private elementRef: ElementRef,
315
        private ngZone: NgZone,
316
        private changeDetectorRef: ChangeDetectorRef
317
    ) {}
318

319
    ngOnInit() {
320
        const bindKeyEventElement = this._getElementBySelector(this.thyBindKeyEventContainer);
321
        this.ngZone.runOutsideAngular(() => {
322
            this._bindKeyEventUnsubscribe = this.renderer.listen(bindKeyEventElement, 'keydown', this.onKeydown.bind(this));
1✔
323
        });
324
        this._instanceSelectionModel();
325
    }
326

327
    writeValue(value: any[] | any): void {
328
        if ((typeof ngDevMode === 'undefined' || ngDevMode) && value) {
329
            if (this.multiple && !helpers.isArray(value)) {
330
                throw new Error(`The multiple selection ngModel must be an array.`);
331
            }
332
            if (!this.multiple && helpers.isArray(value)) {
333
                throw new Error(`The single selection ngModel should not be an array.`);
20✔
334
            }
335
        }
336
        const values = helpers.isArray(value) ? value : value ? [value] : [];
337
        this._modelValues = values;
338
        if (this.options) {
339
            this._setSelectionByValues(values);
340
        }
341
        this.changeDetectorRef.markForCheck();
342
    }
343

344
    registerOnChange(fn: any): void {
345
        this._onChange = fn;
346
    }
347

348
    registerOnTouched(fn: any): void {
349
        this._onTouched = fn;
350
    }
351

352
    setDisabledState(isDisabled: boolean): void {
353
        this.disabled = isDisabled;
354
    }
355

356
    onKeydown(event: KeyboardEvent) {
357
        if (this.thyBeforeKeydown) {
358
            // stop key down event
359
            const isContinue = this.thyBeforeKeydown(event);
360
            if (!isContinue) {
361
                return;
362
            }
363
        }
364
        const keyCode = event.keyCode || event.which;
365
        const manager = this._keyManager;
366
        const previousFocusIndex = manager.activeItemIndex;
367

368
        switch (keyCode) {
369
            case keycodes.SPACE:
370
            case keycodes.ENTER:
371
                if (keyCode === keycodes.SPACE && !this.spaceEnabled) {
372
                    return;
373
                }
374
                this._toggleFocusedOption(event);
375
                // Always prevent space from scrolling the page since the list has focus
376
                event.preventDefault();
377
                break;
378
            default:
379
                manager.onKeydown(event);
380
        }
381
        if (
382
            (keyCode === keycodes.UP_ARROW || keyCode === keycodes.DOWN_ARROW) &&
383
            event.shiftKey &&
384
            manager.activeItemIndex !== previousFocusIndex
385
        ) {
386
            this._toggleFocusedOption(event);
387
        }
388
    }
389

390
    toggleOption(option: ThyListOption, event?: Event) {
391
        if (option && !option.disabled) {
392
            this.selectionModel.toggle(this._getOptionSelectionValue(option));
393
            // Emit a change event because the focused option changed its state through user
394
            // interaction.
395
            this._emitModelValueChange();
396
            this._emitChangeEvent(option, event);
397
        }
398
    }
399

400
    setActiveOption(option: ThyListOption) {
401
        this._keyManager.updateActiveItem(option); // .updateActiveItemIndex(this._getOptionIndex(option));
402
    }
403

404
    scrollIntoView(option: ThyListOption) {
405
        const scrollContainerElement = dom.getHTMLElementBySelector(this.thyScrollContainer, this.elementRef);
406
        ScrollToService.scrollToElement(option.element.nativeElement, scrollContainerElement);
407
    }
408

409
    isSelected(option: ThyListOption) {
410
        return this.selectionModel.isSelected(this._getOptionSelectionValue(option));
411
    }
412

413
    clearActiveItem() {
414
        if (this._keyManager.activeItem) {
415
            this._keyManager.setActiveItem(-1);
416
        }
417
    }
418

419
    determineClearActiveItem() {
420
        if (!this._getActiveOption()) {
421
            this.clearActiveItem();
422
        }
423
    }
424

425
    /** Selects all of the options. */
426
    selectAll() {
427
        this._setAllOptionsSelected(true);
428
    }
429

430
    /** Deselects all of the options. */
431
    deselectAll() {
432
        this._setAllOptionsSelected(false);
433
    }
434

435
    ngAfterContentInit(): void {
436
        this._initializeFocusKeyManager();
437
        this.options.changes.pipe(startWith(true)).subscribe(() => {
438
            if (this.autoActiveFirstItem) {
439
                if (!this._keyManager.activeItem || this.options.toArray().indexOf(this._keyManager.activeItem) < 0) {
440
                    this._keyManager.setFirstItemActive();
441
                }
442
            }
443
        });
444
    }
445

446
    ngOnDestroy() {
447
        this._selectionChangesUnsubscribe$.unsubscribe();
448
        if (this._bindKeyEventUnsubscribe) {
449
            this._bindKeyEventUnsubscribe();
450
        }
451
    }
452
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc