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

atinc / ngx-tethys / d9ae709b-3c27-4b69-b125-b8b80b54f90b

pending completion
d9ae709b-3c27-4b69-b125-b8b80b54f90b

Pull #2757

circleci

mengshuicmq
fix: fix code review
Pull Request #2757: feat(color-picker): color-picker support disabled (#INFR-8645)

98 of 6315 branches covered (1.55%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

2392 of 13661 relevant lines covered (17.51%)

83.12 hits per line

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

2.16
/src/list/selection/selection-list.ts
1
import { ScrollToService } from 'ngx-tethys/core';
2
import { IThyListOptionParentComponent, THY_LIST_OPTION_PARENT_COMPONENT, ThyListLayout, ThyListOptionComponent } from 'ngx-tethys/shared';
3
import { coerceBooleanProperty, 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
    ChangeDetectionStrategy,
12
    ChangeDetectorRef,
13
    Component,
1✔
14
    ContentChildren,
15
    ElementRef,
16
    EventEmitter,
17
    forwardRef,
18
    HostBinding,
19
    Input,
20
    NgZone,
1✔
21
    OnDestroy,
22
    OnInit,
×
23
    Output,
×
24
    QueryList,
×
25
    Renderer2
×
26
} from '@angular/core';
27
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
28

29
import { ThySelectionListChange } from './selection.interface';
×
30

×
31
export type ThyListSize = 'sm' | 'md' | 'lg';
32

33
const listSizesMap = {
×
34
    sm: 'thy-list-sm'
35
};
36

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

61
    private _selectionChangesUnsubscribe$ = Subscription.EMPTY;
62

×
63
    private _bindKeyEventUnsubscribe: () => void;
×
64

65
    private _modelValues: any[];
66

67
    private hostRenderer = useHostRenderer();
68

×
69
    /** The currently selected options. */
×
70
    selectionModel: SelectionModel<any>;
×
71

×
72
    disabled: boolean;
73

×
74
    layout: ThyListLayout = 'list';
75

76
    @HostBinding(`class.thy-list`) _isList = true;
77

×
78
    @HostBinding(`class.thy-selection-list`) _isSelectionList = true;
×
79

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

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

84
    /**
×
85
     * @internal
86
     */
87
    @ContentChildren(ThyListOptionComponent, { descendants: true }) options: QueryList<ThyListOptionComponent>;
88

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

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

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

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

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

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

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

×
141
    /**
142
     * 是否自动激活第一项
143
     */
144
    @Input() set thyAutoActiveFirstItem(value: boolean) {
×
145
        this.autoActiveFirstItem = coerceBooleanProperty(value);
×
146
    }
147

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

156
    private spaceEnabled = true;
157

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

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

×
171
    private autoActiveFirstItem: boolean;
×
172

×
173
    private _onTouched: () => void = () => {};
×
174

×
175
    private _onChange: (value: any) => void = (_: any) => {};
×
176

×
177
    private _emitChangeEvent(option: ThyListOptionComponent, event: Event) {
×
178
        this.thySelectionChange.emit({
×
179
            source: this,
×
180
            value: option.thyValue,
×
181
            option: option,
×
182
            event: event,
183
            selected: this.isSelected(option)
184
        });
×
185
    }
×
186

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

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

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

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

234
    private _getElementBySelector(element: HTMLElement | ElementRef | string): HTMLElement {
×
235
        return dom.getHTMLElementBySelector(element, this.elementRef);
×
236
    }
237

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

249
    private _getOptionSelectionValue(option: ThyListOptionComponent) {
250
        if (option.thyValue) {
×
251
            return this.thyUniqueKey ? option.thyValue[this.thyUniqueKey] : option.thyValue;
×
252
        } else {
253
            return option;
254
        }
255
    }
×
256

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

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

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

×
281
        if (hasChanged) {
282
            this._emitModelValueChange();
283
        }
×
284
    }
×
285

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

445
    ngOnDestroy() {
446
        this._selectionChangesUnsubscribe$.unsubscribe();
447
        if (this._bindKeyEventUnsubscribe) {
448
            this._bindKeyEventUnsubscribe();
449
        }
450
    }
451
}
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