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

atinc / ngx-tethys / f809b15f-539a-4d47-ba33-3b3f2e5d90a3

05 Dec 2023 03:14AM UTC coverage: 90.442% (+0.09%) from 90.349%
f809b15f-539a-4d47-ba33-3b3f2e5d90a3

Pull #2894

circleci

smile1016
feat(cascader): fix error
Pull Request #2894: feat(cascader): add service for cascader #INFR-10254

5340 of 6565 branches covered (0.0%)

Branch coverage included in aggregate %.

300 of 314 new or added lines in 2 files covered. (95.54%)

17 existing lines in 1 file now uncovered.

13319 of 14066 relevant lines covered (94.69%)

974.19 hits per line

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

86.13
/src/cascader/cascader.component.ts
1
import {
2
    EXPANDED_DROPDOWN_POSITIONS,
3
    InputBoolean,
4
    InputNumber,
5
    ScrollToService,
6
    TabIndexDisabledControlValueAccessorMixin,
7
    ThyClickDispatcher
8
} from 'ngx-tethys/core';
9
import { ThyEmptyComponent } from 'ngx-tethys/empty';
10
import { ThyIconComponent } from 'ngx-tethys/icon';
11
import { SelectControlSize, SelectOptionBase, ThySelectControlComponent } from 'ngx-tethys/shared';
12
import { coerceBooleanProperty, elementMatchClosest, isEmpty } from 'ngx-tethys/util';
13
import { BehaviorSubject, Subject } from 'rxjs';
14
import { debounceTime, distinctUntilChanged, filter, take, takeUntil } from 'rxjs/operators';
15
import {
16
    CdkConnectedOverlay,
17
    CdkOverlayOrigin,
18
    ConnectedOverlayPositionChange,
19
    ConnectionPositionPair,
20
    ViewportRuler
21
} from '@angular/cdk/overlay';
22
import { NgClass, NgFor, NgIf, NgStyle, NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
23
import {
1✔
24
    ChangeDetectorRef,
25
    Component,
56✔
26
    ElementRef,
56✔
27
    EventEmitter,
56✔
28
    forwardRef,
4✔
29
    HostListener,
30
    Inject,
31
    Input,
32
    NgZone,
3✔
33
    OnChanges,
3✔
34
    OnDestroy,
3✔
35
    OnInit,
36
    Output,
37
    PLATFORM_ID,
4✔
38
    QueryList,
39
    SimpleChanges,
40
    TemplateRef,
34✔
41
    ViewChild,
42
    ViewChildren
43
} from '@angular/core';
49✔
44
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
45
import { useHostRenderer } from '@tethys/cdk/dom';
46

31✔
47
import { ThyCascaderOptionComponent } from './cascader-li.component';
31✔
48
import { ThyCascaderSearchOptionComponent } from './cascader-search-option.component';
49
import { ThyCascaderExpandTrigger, ThyCascaderOption, ThyCascaderSearchOption, ThyCascaderTriggerType } from './types';
50
import { ThyCascaderService } from './cascader.service';
300✔
51

52
/**
53
 * 级联选择菜单
31✔
54
 * @name thy-cascader
31✔
55
 */
56
@Component({
57
    selector: 'thy-cascader,[thy-cascader]',
98✔
58
    templateUrl: './cascader.component.html',
59
    providers: [
60
        {
416✔
61
            provide: NG_VALUE_ACCESSOR,
62
            useExisting: forwardRef(() => ThyCascaderComponent),
63
            multi: true
46✔
64
        },
65
        ThyCascaderService
66
    ],
32✔
67
    host: {
68
        '[attr.tabindex]': `tabIndex`,
69
        '(focus)': 'onFocus($event)',
45✔
70
        '(blur)': 'onBlur($event)'
45✔
71
    },
72
    standalone: true,
73
    imports: [
2,406✔
74
        CdkOverlayOrigin,
75
        NgIf,
76
        ThySelectControlComponent,
502✔
77
        NgClass,
502✔
78
        NgTemplateOutlet,
79
        CdkConnectedOverlay,
80
        NgStyle,
28✔
81
        NgFor,
82
        ThyCascaderOptionComponent,
UNCOV
83
        ThyCascaderSearchOptionComponent,
×
84
        ThyEmptyComponent,
85
        ThyIconComponent
86
    ]
649✔
87
})
88
export class ThyCascaderComponent
89
    extends TabIndexDisabledControlValueAccessorMixin
49✔
90
    implements ControlValueAccessor, OnInit, OnChanges, OnDestroy
49✔
91
{
49✔
92
    /**
49✔
93
     * 选项的实际值的属性名
49✔
94
     */
49✔
95
    @Input() thyValueProperty = 'value';
49✔
96

97
    /**
98
     * 选项的显示值的属性名
99
     */
100
    @Input() thyLabelProperty = 'label';
101

102
    /**
103
     * 描述输入字段预期值的简短的提示信息
49✔
104
     */
49✔
105
    @Input() thyPlaceholder = '请选择';
106

107
    /**
UNCOV
108
     * 控制大小(4种)
×
UNCOV
109
     * @type 'sm' | 'md' | 'lg' | ''
×
UNCOV
110
     */
×
111
    @Input() thySize: SelectControlSize = '';
112

113
    /**
49✔
114
     * 数据项
9!
115
     * @type ThyCascaderOption[]
9✔
116
     * @default []
9✔
117
     */
1✔
118
    @Input()
119
    set thyOptions(options: ThyCascaderOption[] | null) {
9✔
120
        const columns = options && options.length ? [options] : [];
9✔
121
        this.thyCascaderService.initColumns(columns);
122
        if (this.thyCascaderService.defaultValue && columns.length) {
123
            this.thyCascaderService.initOptions(0);
49!
124
        }
49✔
125
    }
126

127
    /**
128
     * 点击父级菜单选项时,可通过该函数判断是否允许值的变化
42✔
129
     */
130
    @Input() thyChangeOn: (option: ThyCascaderOption, level: number) => boolean;
131

1✔
132
    /**
1✔
133
     * 点击项时,表单是否动态展示数据项
1✔
134
     * @type boolean
135
     */
136
    @Input() @InputBoolean() thyChangeOnSelect = false;
137

138
    /**
139
     * 显示输入框
140
     * @type boolean
74✔
141
     */
34✔
142
    @Input() @InputBoolean() thyShowInput = true;
143

144
    /**
145
     * 用户自定义模板
49✔
146
     * @type TemplateRef
196✔
147
     */
148
    @Input()
49✔
149
    set thyLabelRender(value: TemplateRef<any>) {
49✔
150
        this.labelRenderTpl = value;
49✔
151
        this.isLabelRenderTemplate = value instanceof TemplateRef;
49✔
152
        this.thyCascaderService.setCascaderOptions({ isLabelRenderTemplate: this.isLabelRenderTemplate });
49✔
153
    }
154

155
    get thyLabelRender(): TemplateRef<any> {
108✔
156
        return this.labelRenderTpl;
108✔
157
    }
30✔
158

159
    /**
160
     * 用于动态加载选项
161
     */
49✔
162
    @Input() set thyLoadData(value: (node: ThyCascaderOption, index?: number) => PromiseLike<any>) {
163
        this.thyCascaderService.setCascaderOptions({ loadData: value });
164
    }
218✔
165

218✔
166
    get thyLoadData() {
1✔
167
        return this.thyCascaderService?.cascaderOptions?.loadData;
1✔
168
    }
169

170
    /**
171
     * 控制触发状态, 支持 `click` | `hover`
1,381✔
172
     * @type ThyCascaderTriggerType | ThyCascaderTriggerType[]
173
     */
174
    @Input() thyTriggerAction: ThyCascaderTriggerType | ThyCascaderTriggerType[] = ['click'];
1,381✔
175

176
    /**
177
     * 鼠标经过下方列表项时,是否自动展开列表,支持 `click` | `hover`
1,381✔
178
     * @type ThyCascaderExpandTrigger | ThyCascaderExpandTrigger[]
179
     */
180
    @Input() thyExpandTriggerAction: ThyCascaderExpandTrigger | ThyCascaderExpandTrigger[] = ['click'];
32✔
181

32✔
182
    /**
32✔
183
     * 自定义浮层样式
184
     */
185
    @Input() thyMenuStyle: { [key: string]: string };
186

40✔
187
    /**
15✔
188
     * 自定义浮层类名
79✔
189
     * @type string
190
     */
191
    @Input()
15✔
192
    set thyMenuClassName(value: string) {
32✔
193
        this.menuClassName = value;
29✔
194
        this.setMenuClass();
29✔
195
    }
196

197
    get thyMenuClassName(): string {
198
        return this.menuClassName;
199
    }
200

40✔
201
    /**
39✔
202
     * 自定义浮层列类名
39✔
203
     * @type string
39✔
204
     */
39✔
205
    @Input()
39✔
206
    set thyColumnClassName(value: string) {
33✔
207
        this.columnClassName = value;
208
        this.setMenuClass();
39✔
209
    }
210

211
    get thyColumnClassName(): string {
212
        return this.columnClassName;
271✔
213
    }
214

215
    /**
150✔
216
     * 是否只读
217
     * @default false
218
     */
219
    @Input()
220
    // eslint-disable-next-line prettier/prettier
221
    override get thyDisabled(): boolean {
222
        return this.disabled;
223
    }
502✔
224

225
    override set thyDisabled(value: boolean) {
226
        this.disabled = coerceBooleanProperty(value);
49✔
227
    }
228

229
    disabled = false;
230

231
    /**
232
     * 空状态下的展示文字
360✔
233
     * @default 暂无可选项
234
     */
235
    @Input()
49✔
236
    set thyEmptyStateText(value: string) {
237
        this.emptyStateText = value;
238
    }
239

240
    /**
241
     * 是否多选
242
     * @type boolean
88✔
243
     * @default false
244
     */
245
    @Input()
246
    @InputBoolean()
247
    set thyMultiple(value: boolean) {
248
        this.isMultiple = value;
249
        this.thyCascaderService.setCascaderOptions({ isMultiple: value });
88✔
250
    }
251

252
    get thyMultiple(): boolean {
31✔
253
        return this.isMultiple;
21✔
254
    }
255

10✔
256
    /**
257
     * 设置多选时最大显示的标签数量,0 表示不限制
258
     * @type number
4!
259
     */
4✔
260
    @Input() @InputNumber() thyMaxTagCount = 0;
UNCOV
261

×
262
    /**
263
     * 是否仅允许选择叶子项
264
     * @default true
2!
265
     */
2✔
266
    @Input()
UNCOV
267
    @InputBoolean()
×
268
    thyIsOnlySelectLeaf = true;
269

270
    /**
33✔
271
     * 是否支持搜索
2✔
272
     * @default false
273
     */
31!
274
    @Input() @InputBoolean() thyShowSearch: boolean = false;
31✔
275

276
    /**
277
     * 多选选中项的展示方式,默认为空,渲染文字模板,传入tag,渲染展示模板,
278
     * @default ''|tag
2!
279
     */
×
280
    @Input() thyPreset: string = '';
281

2!
282
    /**
2✔
283
     * 值发生变化时触发,返回选择项的值
284
     * @type EventEmitter<any[]>
285
     */
286
    @Output() thyChange = new EventEmitter<any[]>();
20✔
287

288
    /**
289
     * 值发生变化时触发,返回选择项列表
2!
290
     * @type EventEmitter<ThyCascaderOption[]>
2✔
291
     */
292
    @Output() thySelectionChange = new EventEmitter<ThyCascaderOption[]>();
2!
UNCOV
293

×
294
    /**
295
     * 选择选项时触发
2!
296
     */
1✔
297
    @Output() thySelect = new EventEmitter<{
298
        option: ThyCascaderOption;
1✔
299
        index: number;
300
    }>();
301

2!
302
    /**
2✔
303
     * @private 暂无实现
304
     */
2✔
305
    @Output() thyDeselect = new EventEmitter<{
1✔
306
        option: ThyCascaderOption;
307
        index: number;
1✔
308
    }>();
309

310
    /**
311
     * 清空选项时触发
1!
UNCOV
312
     */
×
313
    @Output() thyClear = new EventEmitter<void>();
314

1✔
315
    /**
316
     * 下拉选项展开和折叠状态事件
317
     */
1!
318
    @Output() thyExpandStatusChange: EventEmitter<boolean> = new EventEmitter<boolean>();
1✔
319

1✔
320
    @ViewChildren('cascaderOptions', { read: ElementRef }) cascaderOptions: QueryList<ElementRef>;
321

322
    @ViewChildren('cascaderOptionContainers', { read: ElementRef }) cascaderOptionContainers: QueryList<ElementRef>;
323

7✔
324
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
1✔
325

1✔
326
    @ViewChild('trigger', { read: ElementRef, static: true }) trigger: ElementRef<any>;
1✔
327

1✔
328
    @ViewChild('input') input: ElementRef;
329

330
    @ViewChild('menu') menu: ElementRef;
4✔
331

4✔
332
    public dropDownPosition = 'bottom';
333

334
    public menuVisible = false;
1✔
335

1✔
336
    public showSearch = false;
337

UNCOV
338
    public isLabelRenderTemplate = false;
×
339

340
    public triggerRect: DOMRect;
341

1!
342
    public emptyStateText = '暂无可选项';
1✔
343

1✔
344
    private prefixCls = 'thy-cascader';
345

1✔
346
    private menuClassName: string;
1✔
347

348
    private columnClassName: string;
349

49✔
350
    private _menuColumnCls: any;
49✔
351

49✔
352
    private readonly destroy$ = new Subject<void>();
49✔
353

49✔
354
    private _menuCls: { [name: string]: any };
49✔
355

49✔
356
    private _labelCls: { [name: string]: any };
49✔
357

49✔
358
    private labelRenderTpl: TemplateRef<any>;
49✔
359

49✔
360
    private hostRenderer = useHostRenderer();
49✔
361

49✔
362
    public positions: ConnectionPositionPair[];
49✔
363

49✔
364
    get selected(): SelectOptionBase | SelectOptionBase[] {
49✔
365
        this.cdkConnectedOverlay?.overlayRef?.updatePosition();
49✔
366
        return this.thyMultiple ? this.thyCascaderService.selectionModel.selected : this.thyCascaderService.selectionModel.selected[0];
49✔
367
    }
49✔
368
    private isMultiple = false;
49✔
369

49✔
370
    public menuMinWidth = 122;
49✔
371

49✔
372
    private searchText$ = new BehaviorSubject('');
49✔
373

49✔
374
    public get searchResultList(): ThyCascaderSearchOption[] {
49✔
375
        return this.thyCascaderService.searchResultList;
49✔
376
    }
49✔
377

49✔
378
    public isShowSearchPanel: boolean = false;
49✔
379

49✔
380
    /**
49✔
381
     * 解决搜索&多选的情况下,点击搜索项会导致 panel 闪烁
49✔
382
     * 由于点击后,thySelectedOptions变化,导致 thySelectControl
49✔
383
     * 又会触发 searchFilter 函数,即 resetSearch 会执行
49✔
384
     * 会导致恢复级联状态再变为搜索状态
49✔
385
     */
49✔
386
    private isSelectingSearchState: boolean = false;
49✔
387

49✔
388
    public get isLoading() {
389
        return this.thyCascaderService?.isLoading;
390
    }
391

392
    public get columns() {
393
        return this.thyCascaderService.columns;
394
    }
49✔
395

49✔
396
    ngOnInit(): void {
9✔
397
        this.setClassMap();
9✔
398
        this.setMenuClass();
9!
399
        this.setMenuColumnClass();
9✔
400
        this.setLabelClass();
401
        this.initPosition();
9✔
402
        this.initSearch();
4✔
403
        const options = {
4✔
404
            labelProperty: this.thyLabelProperty,
405
            valueProperty: this.thyValueProperty,
406
            isMultiple: this.isMultiple,
407
            isOnlySelectLeaf: this.thyIsOnlySelectLeaf,
408
            isLabelRenderTemplate: this.isLabelRenderTemplate,
931!
409
            loadData: this.thyLoadData
410
        };
411
        this.thyCascaderService.setCascaderOptions(options);
7✔
412

1✔
413
        this.viewPortRuler
414
            .change(100)
7✔
415
            .pipe(takeUntil(this.destroy$))
416
            .subscribe(() => {
417
                if (this.menuVisible) {
49✔
418
                    this.triggerRect = this.trigger.nativeElement.getBoundingClientRect();
50✔
419
                    this.cdr.markForCheck();
420
                }
6✔
421
            });
422

6✔
423
        this.thyCascaderService.cascaderValueChange().subscribe(options => {
6✔
424
            if (!options.isValueEqual) {
425
                this.onChangeFn(options.value);
426
                if (options.isSelectionModelEmpty) {
427
                    this.thyClear.emit();
6✔
428
                }
429
                this.thySelectionChange.emit(this.thyCascaderService.selectedOptions);
430
                this.thyChange.emit(options.value);
8✔
431
            }
8✔
432
        });
8✔
433
        if (isPlatformBrowser(this.platformId)) {
434
            this.thyClickDispatcher
435
                .clicked(0)
1✔
436
                .pipe(takeUntil(this.destroy$))
1!
UNCOV
437
                .subscribe(event => {
×
UNCOV
438
                    if (
×
439
                        !this.elementRef.nativeElement.contains(event.target) &&
UNCOV
440
                        !this.menu?.nativeElement.contains(event.target as Node) &&
×
441
                        this.menuVisible
442
                    ) {
1✔
443
                        this.ngZone.run(() => {
3✔
444
                            this.closeMenu();
445
                            this.cdr.markForCheck();
1!
UNCOV
446
                        });
×
UNCOV
447
                    }
×
UNCOV
448
                });
×
449
        }
UNCOV
450
    }
×
UNCOV
451

×
NEW
452
    ngOnChanges(changes: SimpleChanges): void {
×
NEW
453
        if (changes['thyIsOnlySelectLeaf']) {
×
454
            this.thyCascaderService.setCascaderOptions({ isOnlySelectLeaf: changes['thyIsOnlySelectLeaf'].currentValue });
455
        }
456
    }
457

1✔
458
    private initPosition() {
459
        const cascaderPosition: ConnectionPositionPair[] = EXPANDED_DROPDOWN_POSITIONS.map(item => {
460
            return { ...item };
461
        });
49✔
462
        cascaderPosition[0].offsetY = 4; // 左下
49✔
463
        cascaderPosition[1].offsetY = 4; // 右下
464
        cascaderPosition[2].offsetY = -4; // 右下
1✔
465
        cascaderPosition[3].offsetY = -4; // 右下
466
        this.positions = cascaderPosition;
467
    }
468

469
    writeValue(value: any): void {
470
        this.thyCascaderService.writeCascaderValue(value);
471
        if (this.isMultiple) {
472
            this.cdr.detectChanges();
473
        }
1✔
474
    }
475

476
    setDisabledState(isDisabled: boolean): void {
477
        this.disabled = isDisabled;
478
    }
479

480
    public positionChange(position: ConnectedOverlayPositionChange): void {
481
        const newValue = position.connectionPair.originY === 'bottom' ? 'bottom' : 'top';
482
        if (this.dropDownPosition !== newValue) {
483
            this.dropDownPosition = newValue;
484
            this.cdr.detectChanges();
485
        }
486
    }
487

488
    public isActivatedOption(option: ThyCascaderOption, index: number): boolean {
489
        return this.thyCascaderService.isActivatedOption(option, index);
490
    }
491

492
    public isHalfSelectedOption(option: ThyCascaderOption, index: number): boolean {
493
        return this.thyCascaderService.isHalfSelectedOption(option, index);
494
    }
495

496
    public isSelectedOption(option: ThyCascaderOption, index: number): boolean {
497
        return this.thyCascaderService.isSelectedOption(option, index);
498
    }
499

500
    public attached(): void {
501
        this.cdr.detectChanges();
502
        this.cdkConnectedOverlay.positionChange.pipe(take(1), takeUntil(this.destroy$)).subscribe(() => {
503
            this.scrollActiveElementIntoView();
504
        });
505
    }
506

507
    private scrollActiveElementIntoView() {
508
        if (!isEmpty(this.thyCascaderService.selectedOptions)) {
509
            const activeOptions = this.cascaderOptions
510
                .filter(item => item.nativeElement.classList.contains('thy-cascader-menu-item-active'))
511
                // for multiple mode
512
                .slice(-this.cascaderOptionContainers.length);
1✔
513

514
            this.cascaderOptionContainers.forEach((item, index) => {
515
                if (index <= activeOptions.length - 1) {
516
                    ScrollToService.scrollToElement(activeOptions[index].nativeElement, item.nativeElement);
1✔
517
                    this.cdr.detectChanges();
518
                }
519
            });
520
        }
1✔
521
    }
522

523
    public setMenuVisible(menuVisible: boolean): void {
524
        if (this.menuVisible !== menuVisible) {
525
            this.menuVisible = menuVisible;
1✔
526

527
            this.thyCascaderService.initActivatedOptions(this.menuVisible);
528
            this.setClassMap();
529
            this.setMenuClass();
1✔
530
            if (this.menuVisible) {
531
                this.triggerRect = this.trigger.nativeElement.getBoundingClientRect();
532
            }
533
            this.thyExpandStatusChange.emit(menuVisible);
1✔
534
        }
535
    }
536

537
    public get menuCls(): any {
1✔
538
        return this._menuCls;
539
    }
540

541
    private setMenuClass(): void {
542
        this._menuCls = {
543
            [`${this.prefixCls}-menus`]: true,
544
            [`${this.prefixCls}-menus-hidden`]: !this.menuVisible,
49✔
545
            [`${this.thyMenuClassName}`]: this.thyMenuClassName,
546
            [`w-100`]: this.columns.length === 0
547
        };
548
    }
549

550
    public get menuColumnCls(): any {
551
        return this._menuColumnCls;
552
    }
553

554
    private setMenuColumnClass(): void {
555
        this._menuColumnCls = {
556
            [`${this.prefixCls}-menu`]: true,
557
            [`${this.thyColumnClassName}`]: this.thyColumnClassName
558
        };
559
    }
560

561
    public get labelCls(): any {
562
        return this._labelCls;
563
    }
564

565
    private setLabelClass(): void {
566
        this._labelCls = {
567
            [`${this.prefixCls}-picker-label`]: true,
568
            [`${this.prefixCls}-show-search`]: false,
569
            [`${this.prefixCls}-focused`]: false
570
        };
571
    }
572

573
    private setClassMap(): void {
574
        const classMap = {
575
            [`${this.prefixCls}`]: true,
576
            [`${this.prefixCls}-picker`]: true,
577
            [`${this.prefixCls}-${this.thySize}`]: true,
578
            [`${this.prefixCls}-picker-disabled`]: this.disabled,
579
            [`${this.prefixCls}-picker-open`]: this.menuVisible
580
        };
581
        this.hostRenderer.updateClassByMap(classMap);
582
    }
583

584
    private isClickTriggerAction(): boolean {
585
        if (typeof this.thyTriggerAction === 'string') {
586
            return this.thyTriggerAction === 'click';
587
        }
588
        return this.thyTriggerAction.indexOf('click') !== -1;
589
    }
590

591
    private isHoverTriggerAction(): boolean {
592
        if (typeof this.thyTriggerAction === 'string') {
593
            return this.thyTriggerAction === 'hover';
594
        }
595
        return this.thyTriggerAction.indexOf('hover') !== -1;
596
    }
597

598
    private isHoverExpandTriggerAction(): boolean {
599
        if (typeof this.thyExpandTriggerAction === 'string') {
600
            return this.thyExpandTriggerAction === 'hover';
601
        }
602
        return this.thyExpandTriggerAction.indexOf('hover') !== -1;
603
    }
604

605
    @HostListener('click', ['$event'])
606
    public toggleClick($event: Event) {
607
        if (this.disabled) {
608
            return;
609
        }
610
        if (this.isClickTriggerAction()) {
611
            this.setMenuVisible(!this.menuVisible);
612
        }
613
    }
614

615
    @HostListener('mouseover', ['$event'])
616
    public toggleHover($event: Event) {
617
        if (this.disabled) {
618
            return;
619
        }
620
        if (this.isHoverTriggerAction()) {
621
            this.setMenuVisible(!this.menuVisible);
622
        }
623
    }
624

625
    public clickOption(option: ThyCascaderOption, index: number, event: Event | boolean): void {
626
        this.thyCascaderService.clickOption(option, index, event, this.selectOption);
627
    }
628

629
    public mouseoverOption(option: ThyCascaderOption, index: number, event: Event): void {
630
        if (event) {
631
            event.preventDefault();
632
        }
633

634
        if (option && option.disabled && !this.isMultiple) {
635
            return;
636
        }
637

638
        if (!this.isHoverExpandTriggerAction() && !(option && option.disabled && this.isMultiple)) {
639
            return;
640
        }
641
        this.setActiveOption(option, index, false);
642
    }
643

644
    public mouseleaveMenu(event: Event) {
645
        if (event) {
646
            event.preventDefault();
647
        }
648
        if (!this.isHoverTriggerAction()) {
649
            return;
650
        }
651
        this.setMenuVisible(!this.menuVisible);
652
    }
653

654
    onBlur(event?: FocusEvent) {
655
        // Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
656
        if (elementMatchClosest(event?.relatedTarget as HTMLElement, ['.thy-cascader-menus', 'thy-cascader'])) {
657
            return;
658
        }
659
        this.onTouchedFn();
660
    }
661

662
    onFocus(event?: FocusEvent) {
663
        if (!elementMatchClosest(event?.relatedTarget as HTMLElement, ['.thy-cascader-menus', 'thy-cascader'])) {
664
            const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
665
            inputElement.focus();
666
        }
667
    }
668

669
    public closeMenu(): void {
670
        if (this.menuVisible) {
671
            this.setMenuVisible(false);
672
            this.onTouchedFn();
673
            this.isShowSearchPanel = false;
674
            this.thyCascaderService.searchResultList = [];
675
        }
676
    }
677

678
    public setActiveOption(option: ThyCascaderOption, index: number, select: boolean, loadChildren: boolean = true): void {
679
        this.thyCascaderService.setActiveOption(option, index, select, loadChildren, this.selectOption);
680
    }
681

682
    private selectOption = (option: ThyCascaderOption, index: number) => {
683
        this.thySelect.emit({ option, index });
684
        const isOptionCanSelect = this.thyChangeOnSelect && !this.isMultiple;
685
        if (option.isLeaf || !this.thyIsOnlySelectLeaf || isOptionCanSelect || this.shouldPerformSelection(option, index)) {
686
            this.thyCascaderService.selectOption(option, index);
687
        }
688
        if ((option.isLeaf || !this.thyIsOnlySelectLeaf) && !this.thyMultiple) {
689
            this.setMenuVisible(false);
690
            this.onTouchedFn();
691
        }
692
    };
693

694
    public removeSelectedItem(event: { item: SelectOptionBase; $eventOrigin: Event }) {
695
        event.$eventOrigin.stopPropagation();
696
        this.thyCascaderService.removeSelectedItem(event?.item);
697
    }
698

699
    private shouldPerformSelection(option: ThyCascaderOption, level: number): boolean {
700
        return typeof this.thyChangeOn === 'function' ? this.thyChangeOn(option, level) === true : false;
701
    }
702

703
    public clearSelection($event: Event): void {
704
        if ($event) {
705
            $event.stopPropagation();
706
            $event.preventDefault();
707
        }
708
        this.thyCascaderService.clearSelection();
709
        this.setMenuVisible(false);
710
    }
711

712
    constructor(
713
        @Inject(PLATFORM_ID) private platformId: string,
714
        private cdr: ChangeDetectorRef,
715
        private viewPortRuler: ViewportRuler,
716
        public elementRef: ElementRef,
717
        private thyClickDispatcher: ThyClickDispatcher,
718
        private ngZone: NgZone,
719
        public thyCascaderService: ThyCascaderService
720
    ) {
721
        super();
722
    }
723

724
    public trackByFn(index: number, item: ThyCascaderOption) {
725
        return item?.value || item?._id || index;
726
    }
727

728
    public searchFilter(searchText: string) {
729
        if (!searchText && !this.isSelectingSearchState) {
730
            this.resetSearch();
731
        }
732
        this.searchText$.next(searchText);
733
    }
734

735
    private initSearch() {
736
        this.searchText$
737
            .pipe(
738
                takeUntil(this.destroy$),
739
                debounceTime(200),
740
                distinctUntilChanged(),
741
                filter(text => text !== '')
742
            )
743
            .subscribe(searchText => {
744
                this.resetSearch();
745

746
                // local search
747
                this.searchInLocal(searchText);
748
                this.isShowSearchPanel = true;
749
            });
750
    }
751

752
    private searchInLocal(searchText: string): void {
753
        this.thyCascaderService.searchInLocal(searchText);
754
    }
755

756
    private resetSearch() {
757
        this.isShowSearchPanel = false;
758
        this.thyCascaderService.resetSearch();
759
        this.scrollActiveElementIntoView();
760
    }
761

762
    public selectSearchResult(selectOptionData: ThyCascaderSearchOption): void {
763
        const { thyRowValue: selectedOptions } = selectOptionData;
764
        if (selectOptionData.selected) {
765
            if (!this.isMultiple) {
766
                this.closeMenu();
767
            }
768
            return;
769
        }
770
        selectedOptions.forEach((item: ThyCascaderOption, index: number) => {
771
            this.setActiveOption(item, index, index === selectedOptions.length - 1);
772
        });
773
        if (this.isMultiple) {
774
            this.isSelectingSearchState = true;
775
            selectOptionData.selected = true;
776
            const originSearchResultList = this.searchResultList;
777
            // 保持搜索选项
778
            setTimeout(() => {
779
                this.isShowSearchPanel = true;
780
                this.thyCascaderService.searchResultList = originSearchResultList;
781
                this.isSelectingSearchState = false;
782
            });
783
        } else {
784
            this.resetSearch();
785
        }
786
    }
787

788
    ngOnDestroy() {
789
        this.destroy$.next();
790
        this.destroy$.complete();
791
    }
792
}
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