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

atinc / ngx-tethys / 8e425d8e-ed07-4da4-b70f-52484a7a281a

22 Nov 2024 04:41AM UTC coverage: 90.357% (+0.006%) from 90.351%
8e425d8e-ed07-4da4-b70f-52484a7a281a

Pull #3272

circleci

web-flow
Merge branch 'master' into wumeimin/empty-theme
Pull Request #3272: feat: empty icon use 'preset-light' in dark theme #TINFR-975

5547 of 6789 branches covered (81.71%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 3 files covered. (100.0%)

35 existing lines in 4 files now uncovered.

13260 of 14025 relevant lines covered (94.55%)

992.7 hits per line

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

91.76
/src/autocomplete/autocomplete.component.ts
1
import {
2
    Component,
3
    TemplateRef,
4
    ViewChild,
5
    ChangeDetectionStrategy,
6
    ContentChildren,
7
    QueryList,
8
    OnInit,
9
    Output,
10
    EventEmitter,
11
    NgZone,
12
    OnDestroy,
13
    AfterContentInit,
14
    ChangeDetectorRef,
15
    Input,
16
    ElementRef,
17
    inject,
18
    Signal
1✔
19
} from '@angular/core';
20
import { defer, merge, Observable, Subject, timer } from 'rxjs';
15✔
21
import { take, switchMap, takeUntil, startWith } from 'rxjs/operators';
15✔
22
import { SelectionModel } from '@angular/cdk/collections';
15✔
23
import {
15✔
24
    THY_OPTION_PARENT_COMPONENT,
15✔
25
    IThyOptionParentComponent,
15✔
26
    ThyOption,
15✔
27
    ThyOptionSelectionChangeEvent,
15✔
28
    ThyStopPropagationDirective
15✔
29
} from 'ngx-tethys/shared';
15✔
30
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
15!
31
import { ThyEmpty } from 'ngx-tethys/empty';
102✔
32
import { NgClass } from '@angular/common';
UNCOV
33
import { coerceBooleanProperty } from 'ngx-tethys/util';
×
34
import { injectLocale, ThyAutoCompleteLocale } from 'ngx-tethys/i18n';
35
import { ThyThemeStore } from 'ngx-tethys/core';
15✔
36

15✔
37
/** Event object that is emitted when an autocomplete option is activated. */
15✔
38
export interface ThyAutocompleteActivatedEvent {
15✔
39
    /** Reference to the autocomplete panel that emitted the event. */
15✔
40
    source: ThyAutocomplete;
41

42
    /** Option that was selected. */
15✔
43
    option: ThyOption | null;
15✔
44
}
45

46
/**
15✔
47
 * 自动完成组件
15✔
48
 * @name thy-autocomplete
15✔
49
 */
15✔
50
@Component({
15✔
51
    selector: 'thy-autocomplete',
52
    templateUrl: 'autocomplete.component.html',
15✔
53
    changeDetection: ChangeDetectionStrategy.OnPush,
54
    providers: [
55
        {
56
            provide: THY_OPTION_PARENT_COMPONENT,
15✔
57
            useExisting: ThyAutocomplete
15✔
58
        }
15✔
59
    ],
8✔
60
    standalone: true,
61
    imports: [ThyStopPropagationDirective, NgClass, ThyEmpty]
62
})
63
export class ThyAutocomplete implements IThyOptionParentComponent, OnInit, AfterContentInit, OnDestroy {
11✔
64
    private ngZone = inject(NgZone);
11✔
65
    private changeDetectorRef = inject(ChangeDetectorRef);
11✔
66

67
    private ngUnsubscribe$ = new Subject<void>();
68

11✔
69
    private locale: Signal<ThyAutoCompleteLocale> = injectLocale('autocomplete');
11✔
70

71
    thyThemeStore = inject(ThyThemeStore);
72

15✔
73
    dropDownClass: { [key: string]: boolean };
15✔
74

5✔
75
    isMultiple = false;
76

77
    mode = '';
78

15!
UNCOV
79
    isEmptyOptions = false;
×
80

81
    selectionModel: SelectionModel<ThyOption>;
15✔
82

15✔
83
    isOpened = false;
3✔
84

3✔
85
    /** Manages active item in option list based on key events. */
86
    keyManager: ActiveDescendantKeyManager<ThyOption>;
87

88
    @ViewChild('contentTemplate', { static: true })
5✔
89
    contentTemplateRef: TemplateRef<any>;
5✔
90

2✔
91
    // scroll element container
2✔
92
    @ViewChild('panel')
93
    optionsContainer: ElementRef<any>;
94

3!
95
    /**
3!
96
     * @private
97
     */
3!
98
    @ContentChildren(ThyOption, { descendants: true }) options: QueryList<ThyOption>;
3✔
99

100
    readonly optionSelectionChanges: Observable<ThyOptionSelectionChangeEvent> = defer(() => {
101
        if (this.options) {
5✔
102
            return merge(...this.options.map(option => option.selectionChange));
3✔
103
        }
104
        return this.ngZone.onStable.asObservable().pipe(
5✔
105
            take(1),
106
            switchMap(() => this.optionSelectionChanges)
107
        );
15✔
108
    }) as Observable<ThyOptionSelectionChangeEvent>;
15✔
109

3✔
110
    /**
111
     * 空选项时的文本
112
     * @default 没有任何数据
12✔
113
     */
114
    @Input() thyEmptyText = this.locale().empty;
15✔
115

116
    /**
117
     * 是否默认高亮第一个选项
118
     * @type boolean
119
     * @default false
120
     */
15✔
121
    @Input({ transform: coerceBooleanProperty }) thyAutoActiveFirstOption: boolean;
15✔
122

123
    /**
1✔
124
     * 被选中时调用,参数包含选中项的 value 值
125
     * @type EventEmitter<ThyOptionSelectionChangeEvent>
126
     */
127
    @Output() thyOptionSelected: EventEmitter<ThyOptionSelectionChangeEvent> = new EventEmitter<ThyOptionSelectionChangeEvent>();
128

129
    /**
130
     * 只读,展开下拉菜单的回调
131
     * @type EventEmitter<void>
132
     */
133
    @Output() readonly thyOpened: EventEmitter<void> = new EventEmitter<void>();
134

135
    /**
1✔
136
     * 只读,关闭下拉菜单的回调
137
     * @type EventEmitter<void>
138
     */
139
    @Output() readonly thyClosed: EventEmitter<void> = new EventEmitter<void>();
140

141
    /** Emits whenever an option is activated using the keyboard. */
142
    /**
143
     * 只读,option 激活状态变化时,调用此函数
144
     * @type EventEmitter<ThyAutocompleteActivatedEvent>
145
     */
146
    @Output() readonly thyOptionActivated: EventEmitter<ThyAutocompleteActivatedEvent> = new EventEmitter<ThyAutocompleteActivatedEvent>();
147

148
    ngOnInit() {
149
        this.setDropDownClass();
150
        this.instanceSelectionModel();
151
    }
152

153
    ngAfterContentInit() {
154
        this.options.changes.pipe(startWith(null), takeUntil(this.ngUnsubscribe$)).subscribe(() => {
155
            this.resetOptions();
156
            timer(0).subscribe(() => {
157
                this.isEmptyOptions = this.options.length <= 0;
158
                this.changeDetectorRef.detectChanges();
159
            });
160
            this.initKeyManager();
161
        });
162
    }
163

164
    initKeyManager() {
165
        const changedOrDestroyed$ = merge(this.options.changes, this.ngUnsubscribe$);
166
        this.keyManager = new ActiveDescendantKeyManager<ThyOption>(this.options).withWrap();
167
        this.keyManager.change.pipe(takeUntil(changedOrDestroyed$)).subscribe(index => {
168
            this.thyOptionActivated.emit({ source: this, option: this.options.toArray()[index] || null });
169
        });
170
    }
171

172
    open() {
173
        this.isOpened = true;
174
        this.changeDetectorRef.markForCheck();
175
        this.thyOpened.emit();
176
    }
177

178
    close() {
179
        this.isOpened = false;
180
        this.thyClosed.emit();
181
    }
182

183
    private resetOptions() {
184
        const changedOrDestroyed$ = merge(this.options.changes, this.ngUnsubscribe$);
185

186
        this.optionSelectionChanges.pipe(takeUntil(changedOrDestroyed$)).subscribe((event: ThyOptionSelectionChangeEvent) => {
187
            this.onSelect(event.option, event.isUserInput);
188
        });
189
    }
190

191
    private instanceSelectionModel() {
192
        if (this.selectionModel) {
193
            this.selectionModel.clear();
194
        }
195
        this.selectionModel = new SelectionModel<ThyOption>(this.isMultiple);
196
        this.selectionModel.changed.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(event => {
197
            event.added.forEach(option => option.select());
198
            event.removed.forEach(option => option.deselect());
199
        });
200
    }
201

202
    private onSelect(option: ThyOption, isUserInput: boolean) {
203
        const wasSelected = this.selectionModel.isSelected(option);
204

205
        if (option.thyValue == null && !this.isMultiple) {
206
            option.deselect();
207
            this.selectionModel.clear();
208
        } else {
209
            if (wasSelected !== option.selected) {
210
                option.selected ? this.selectionModel.select(option) : this.selectionModel.deselect(option);
211
            }
212

213
            if (isUserInput) {
214
                this.keyManager.setActiveItem(option);
215
            }
216
        }
217

218
        if (wasSelected !== this.selectionModel.isSelected(option)) {
219
            this.thyOptionSelected.emit(new ThyOptionSelectionChangeEvent(option, false));
220
        }
221
        this.changeDetectorRef.markForCheck();
222
    }
223

224
    private setDropDownClass() {
225
        let modeClass = '';
226
        if (this.isMultiple) {
227
            modeClass = `thy-select-dropdown-${this.mode}`;
228
        } else {
229
            modeClass = `thy-select-dropdown-single`;
230
        }
231
        this.dropDownClass = {
232
            [`thy-select-dropdown`]: true,
233
            [modeClass]: true
234
        };
235
    }
236

237
    ngOnDestroy() {
238
        this.ngUnsubscribe$.next();
239
        this.ngUnsubscribe$.complete();
240
    }
241
}
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

© 2026 Coveralls, Inc