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

atinc / ngx-tethys / 410fa9cb-fc2e-4e15-971e-8b276efacdc1

30 Nov 2023 02:01PM UTC coverage: 90.331% (+0.03%) from 90.303%
410fa9cb-fc2e-4e15-971e-8b276efacdc1

Pull #2923

circleci

smile1016
feat(select): choice item remove icon display
Pull Request #2923: feat(select):multi-select selected options support template #INFR-10631

5323 of 6554 branches covered (0.0%)

Branch coverage included in aggregate %.

8 of 12 new or added lines in 3 files covered. (66.67%)

54 existing lines in 3 files now uncovered.

13269 of 14028 relevant lines covered (94.59%)

975.91 hits per line

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

88.02
/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 { Id } from 'ngx-tethys/types';
13
import { coerceBooleanProperty, elementMatchClosest, isArray, isEmpty, set, helpers } from 'ngx-tethys/util';
14
import { BehaviorSubject, Subject } from 'rxjs';
15
import { debounceTime, distinctUntilChanged, filter, take, takeUntil } from 'rxjs/operators';
16

17
import { SelectionModel } from '@angular/cdk/collections';
18
import {
19
    CdkConnectedOverlay,
20
    CdkOverlayOrigin,
21
    ConnectedOverlayPositionChange,
22
    ConnectionPositionPair,
134✔
23
    ViewportRuler
78✔
24
} from '@angular/cdk/overlay';
25
import { NgClass, NgFor, NgIf, NgStyle, NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
56✔
26
import {
6✔
27
    ChangeDetectorRef,
28
    Component,
29
    ElementRef,
50✔
30
    EventEmitter,
31
    forwardRef,
134✔
32
    HostListener,
33
    Inject,
34
    Input,
98✔
35
    NgZone,
74✔
36
    OnDestroy,
37
    OnInit,
24✔
38
    Output,
24✔
39
    PLATFORM_ID,
30✔
40
    QueryList,
20✔
41
    TemplateRef,
42
    ViewChild,
43
    ViewChildren
4✔
44
} from '@angular/core';
45
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
44✔
46
import { useHostRenderer } from '@tethys/cdk/dom';
47

48
import { ThyCascaderOptionComponent } from './cascader-li.component';
49
import { ThyCascaderSearchOptionComponent } from './cascader-search-option.component';
50
import { ThyCascaderExpandTrigger, ThyCascaderOption, ThyCascaderSearchOption, ThyCascaderTriggerType } from './types';
1✔
51
import { deepCopy } from '@angular-devkit/core';
52

55✔
53
function toArray<T>(value: T | T[]): T[] {
55✔
54
    let ret: T[];
4✔
55
    if (value == null) {
56
        ret = [];
57
    } else if (!Array.isArray(value)) {
58
        ret = [value];
3✔
59
    } else {
3✔
60
        ret = value;
61
    }
62
    return ret;
6✔
63
}
64

65
function arrayEquals<T>(array1: T[], array2: T[]): boolean {
30✔
66
    if (!array1 || !array2 || array1.length !== array2.length) {
30✔
67
        return false;
68
    }
69

292✔
70
    const len = array1.length;
71
    for (let i = 0; i < len; i++) {
72
        if (array1[i] !== array2[i]) {
30✔
73
            return false;
30✔
74
        }
75
    }
76
    return true;
96✔
77
}
78

79
const defaultDisplayRender = (label: any) => label.join(' / ');
410✔
80

81
/**
82
 * 级联选择菜单
45✔
83
 * @name thy-cascader
84
 */
85
@Component({
31✔
86
    selector: 'thy-cascader,[thy-cascader]',
87
    templateUrl: './cascader.component.html',
88
    providers: [
13✔
89
        {
13✔
90
            provide: NG_VALUE_ACCESSOR,
91
            useExisting: forwardRef(() => ThyCascaderComponent),
92
            multi: true
2,455✔
93
        }
94
    ],
95
    host: {
494✔
96
        '[attr.tabindex]': `tabIndex`,
494✔
97
        '(focus)': 'onFocus($event)',
98
        '(blur)': 'onBlur($event)'
99
    },
48✔
100
    styles: [
48✔
101
        `
48✔
102
            .thy-cascader-menus {
48✔
103
                position: relative;
48✔
104
            }
48✔
105
        `
48✔
106
    ],
35✔
107
    standalone: true,
108
    imports: [
48✔
109
        CdkOverlayOrigin,
110
        NgIf,
111
        ThySelectControlComponent,
UNCOV
112
        NgClass,
×
UNCOV
113
        NgTemplateOutlet,
×
UNCOV
114
        CdkConnectedOverlay,
×
115
        NgStyle,
116
        NgFor,
117
        ThyCascaderOptionComponent,
48✔
118
        ThyCascaderSearchOptionComponent,
9✔
119
        ThyEmptyComponent,
120
        ThyIconComponent
48!
121
    ]
48✔
122
})
123
export class ThyCascaderComponent extends TabIndexDisabledControlValueAccessorMixin implements ControlValueAccessor, OnInit, OnDestroy {
124
    /**
125
     * 选项的实际值的属性名
41✔
126
     */
127
    @Input() thyValueProperty = 'value';
128

1✔
129
    /**
1✔
130
     * 选项的显示值的属性名
1✔
131
     */
132
    @Input() thyLabelProperty = 'label';
133

134
    /**
135
     * 描述输入字段预期值的简短的提示信息
136
     */
137
    @Input() thyPlaceholder = '请选择';
13!
UNCOV
138

×
139
    /**
140
     * 控制大小(4种)
141
     * @type 'sm' | 'md' | 'lg' | ''
13✔
142
     */
143
    @Input() thySize: SelectControlSize = '';
144

145
    /**
48✔
146
     * 数据项
192✔
147
     * @type ThyCascaderOption[]
148
     * @default []
48✔
149
     */
48✔
150
    @Input()
48✔
151
    set thyOptions(options: ThyCascaderOption[] | null) {
48✔
152
        this.columns = options && options.length ? [options] : [];
48✔
153
        if (this.defaultValue && this.columns.length) {
154
            this.initOptions(0);
155
        }
113✔
156
    }
113✔
157

112✔
158
    /**
112✔
159
     * 点击父级菜单选项时,可通过该函数判断是否允许值的变化
68✔
160
     */
161
    @Input() thyChangeOn: (option: ThyCascaderOption, level: number) => boolean;
112✔
162

40✔
163
    /**
164
     * 点击项时,表单是否动态展示数据项
165
     * @type boolean
113✔
166
     */
111✔
167
    @Input() @InputBoolean() thyChangeOnSelect = false;
168

169
    /**
2✔
170
     * 显示输入框
2✔
171
     * @type boolean
172
     */
173
    @Input() @InputBoolean() thyShowInput = true;
174

112✔
175
    /**
112✔
176
     * 用户自定义模板
10✔
177
     * @type TemplateRef
10!
178
     */
179
    @Input()
180
    set thyLabelRender(value: TemplateRef<any>) {
10!
181
        this.labelRenderTpl = value;
10!
182
        this.isLabelRenderTemplate = value instanceof TemplateRef;
183
    }
184

112✔
185
    get thyLabelRender(): TemplateRef<any> {
112✔
186
        return this.labelRenderTpl;
187
    }
188

131✔
189
    /**
112✔
190
     * 用于动态加载选项
33✔
191
     */
192
    @Input() thyLoadData: (node: ThyCascaderOption, index?: number) => PromiseLike<any>;
112✔
193

194
    /**
195
     * 控制触发状态, 支持 `click` | `hover`
19✔
196
     * @type ThyCascaderTriggerType | ThyCascaderTriggerType[]
4✔
197
     */
4✔
UNCOV
198
    @Input() thyTriggerAction: ThyCascaderTriggerType | ThyCascaderTriggerType[] = ['click'];
×
199

200
    /**
4✔
201
     * 鼠标经过下方列表项时,是否自动展开列表,支持 `click` | `hover`
202
     * @type ThyCascaderExpandTrigger | ThyCascaderExpandTrigger[]
19✔
203
     */
10✔
204
    @Input() thyExpandTriggerAction: ThyCascaderExpandTrigger | ThyCascaderExpandTrigger[] = ['click'];
205

206
    /**
9✔
207
     * 自定义浮层样式
208
     */
19✔
209
    @Input() thyMenuStyle: { [key: string]: string };
10✔
210

211
    /**
19✔
212
     * 自定义浮层类名
213
     * @type string
214
     */
215
    @Input()
105!
UNCOV
216
    set thyMenuClassName(value: string) {
×
217
        this.menuClassName = value;
218
        this.setMenuClass();
105✔
219
    }
77✔
220

77✔
221
    get thyMenuClassName(): string {
12✔
222
        return this.menuClassName;
223
    }
224

65✔
225
    /**
65✔
226
     * 自定义浮层列类名
65✔
227
     * @type string
228
     */
229
    @Input()
230
    set thyColumnClassName(value: string) {
28✔
231
        this.columnClassName = value;
28✔
232
        this.setMenuClass();
28✔
233
    }
29✔
234

29!
235
    get thyColumnClassName(): string {
29✔
236
        return this.columnClassName;
237
    }
UNCOV
238

×
UNCOV
239
    /**
×
UNCOV
240
     * 是否只读
×
241
     * @default false
242
     */
243
    @Input()
28✔
244
    // eslint-disable-next-line prettier/prettier
245
    override get thyDisabled(): boolean {
246
        return this.disabled;
247
    }
106✔
248

106✔
249
    override set thyDisabled(value: boolean) {
106✔
250
        this.disabled = coerceBooleanProperty(value);
106✔
251
    }
252

253
    disabled = false;
106✔
254

29✔
255
    /**
87✔
256
     * 空状态下的展示文字
29✔
257
     * @default 暂无可选项
29✔
258
     */
259
    @Input()
260
    set thyEmptyStateText(value: string) {
29✔
261
        this.emptyStateText = value;
262
    }
263

264
    /**
57✔
265
     * 是否多选
115✔
266
     * @type boolean
81✔
267
     * @default false
81✔
268
     */
81✔
269
    @Input()
28✔
270
    @InputBoolean()
271
    set thyMultiple(value: boolean) {
272
        this.isMultiple = value;
273
        this.initSelectionModel();
274
    }
275

48✔
276
    get thyMultiple(): boolean {
277
        return this.isMultiple;
278
    }
215✔
279

215✔
280
    /**
1✔
281
     * 设置多选时最大显示的标签数量,0 表示不限制
1✔
282
     * @type number
283
     */
284
    @Input() @InputNumber() thyMaxTagCount = 0;
285

113✔
286
    /**
287
     * 是否仅允许选择叶子项
288
     * @default true
185!
289
     */
290
    @Input()
291
    @InputBoolean()
353!
292
    thyIsOnlySelectLeaf = true;
293

294
    /**
1,364!
295
     * 是否支持搜索
1,364✔
296
     * @default false
1,364✔
297
     */
298
    @Input() @InputBoolean() thyShowSearch: boolean = false;
UNCOV
299

×
NEW
300
    /**
×
301
     * 多选选中项的展示方式,默认为空,渲染文字模板,传入tag,渲染展示模板,
302
     * @default ''|tag
NEW
303
     */
×
NEW
304
    @Input() thyPreset: string = '';
×
NEW
305

×
UNCOV
306
    /**
×
307
     * 值发生变化时触发,返回选择项的值
UNCOV
308
     * @type EventEmitter<any[]>
×
309
     */
310
    @Output() thyChange = new EventEmitter<any[]>();
311

312
    /**
313
     * 值发生变化时触发,返回选择项列表
1,364✔
314
     * @type EventEmitter<ThyCascaderOption[]>
183✔
315
     */
316
    @Output() thySelectionChange = new EventEmitter<ThyCascaderOption[]>();
1,181✔
317

318
    /**
319
     * 选择选项时触发
1,440✔
320
     */
1,402✔
321
    @Output() thySelect = new EventEmitter<{
306✔
322
        option: ThyCascaderOption;
323
        index: number;
324
    }>();
1,096✔
325

326
    /**
327
     * @private 暂无实现
328
     */
38✔
329
    @Output() thyDeselect = new EventEmitter<{
38✔
330
        option: ThyCascaderOption;
×
UNCOV
331
        index: number;
×
UNCOV
332
    }>();
×
333

334
    /**
UNCOV
335
     * 清空选项时触发
×
336
     */
337
    @Output() thyClear = new EventEmitter<void>();
338

38✔
339
    /**
340
     * 下拉选项展开和折叠状态事件
341
     */
342
    @Output() thyExpandStatusChange: EventEmitter<boolean> = new EventEmitter<boolean>();
31✔
343

31✔
344
    @ViewChildren('cascaderOptions', { read: ElementRef }) cascaderOptions: QueryList<ElementRef>;
31✔
345

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

348
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
39✔
349

15✔
350
    @ViewChild('trigger', { read: ElementRef, static: true }) trigger: ElementRef<any>;
79✔
351

352
    @ViewChild('input') input: ElementRef;
353

15✔
354
    @ViewChild('menu') menu: ElementRef;
32✔
355

29✔
356
    public dropDownPosition = 'bottom';
29✔
357
    public menuVisible = false;
358
    public isLoading = false;
359
    public showSearch = false;
360
    public labelRenderText: string;
361
    public labelRenderContext: any = {};
362
    public isLabelRenderTemplate = false;
112✔
363
    public triggerRect: DOMRect;
112✔
364
    public columns: ThyCascaderOption[][] = [];
109!
365
    public emptyStateText = '暂无可选项';
147✔
366

367
    public selectionModel: SelectionModel<SelectOptionBase>;
3✔
368
    private prefixCls = 'thy-cascader';
369
    private menuClassName: string;
370
    private columnClassName: string;
114✔
371
    private _menuColumnCls: any;
128✔
372
    private defaultValue: any[];
114✔
373
    private readonly destroy$ = new Subject<void>();
66✔
374
    private _menuCls: { [name: string]: any };
375
    private _labelCls: { [name: string]: any };
376
    private labelRenderTpl: TemplateRef<any>;
377
    private hostRenderer = useHostRenderer();
48✔
378
    private cascaderPosition: ConnectionPositionPair[];
4✔
379
    positions: ConnectionPositionPair[];
380

381
    private value: any[];
44✔
382

44✔
383
    private selectedOptions: ThyCascaderOption[] = [];
384

48!
385
    private activatedOptions: ThyCascaderOption[] = [];
48✔
386

387
    get selected(): SelectOptionBase | SelectOptionBase[] {
388
        this.cdkConnectedOverlay?.overlayRef?.updatePosition();
389
        return this.thyMultiple ? this.selectionModel.selected : this.selectionModel.selected[0];
390
    }
391

392
    private isMultiple = false;
393

394
    private prevSelectedOptions: Set<ThyCascaderOption> = new Set<ThyCascaderOption>();
48✔
395

396
    public menuMinWidth = 122;
397

UNCOV
398
    private searchText$ = new BehaviorSubject('');
×
399

400
    public searchResultList: ThyCascaderSearchOption[] = [];
401

39✔
402
    public isShowSearchPanel: boolean = false;
38✔
403

38✔
404
    /**
38✔
405
     * 解决搜索&多选的情况下,点击搜索项会导致 panel 闪烁
38✔
406
     * 由于点击后,thySelectedOptions变化,导致 thySelectControl
38✔
407
     * 又会触发 searchFilter 函数,即 resetSearch 会执行
32✔
408
     * 会导致恢复级联状态再变为搜索状态
409
     */
38✔
410
    private isSelectingSearchState: boolean = false;
411

412
    private flattenOptions: ThyCascaderSearchOption[] = [];
413

38✔
414
    private leafNodes: ThyCascaderSearchOption[] = [];
25✔
415

416
    private valueChange$ = new Subject();
13✔
417

13✔
418
    ngOnInit(): void {
31✔
419
        this.setClassMap();
19✔
420
        this.setMenuClass();
421
        this.setMenuColumnClass();
422
        this.setLabelClass();
423
        this.initPosition();
424
        this.initSearch();
266✔
425
        if (!this.selectionModel) {
426
            this.selectionModel = new SelectionModel<SelectOptionBase>(this.thyMultiple);
427
        }
146✔
428
        this.viewPortRuler
429
            .change(100)
430
            .pipe(takeUntil(this.destroy$))
431
            .subscribe(() => {
432
                if (this.menuVisible) {
433
                    this.triggerRect = this.trigger.nativeElement.getBoundingClientRect();
434
                    this.cdr.markForCheck();
435
                }
495✔
436
            });
437

438
        this.valueChange$.pipe(takeUntil(this.destroy$), debounceTime(100)).subscribe(() => {
48✔
439
            this.valueChange();
440
        });
441

442
        if (isPlatformBrowser(this.platformId)) {
443
            this.thyClickDispatcher
444
                .clicked(0)
361✔
445
                .pipe(takeUntil(this.destroy$))
446
                .subscribe(event => {
447
                    if (
48✔
448
                        !this.elementRef.nativeElement.contains(event.target) &&
449
                        !this.menu?.nativeElement.contains(event.target as Node) &&
450
                        this.menuVisible
451
                    ) {
452
                        this.ngZone.run(() => {
453
                            this.closeMenu();
454
                            this.cdr.markForCheck();
86✔
455
                        });
456
                    }
457
                });
458
        }
459
    }
460

461
    private initSelectionModel() {
86✔
462
        if (this.selectionModel) {
463
            this.selectionModel.clear();
464
        } else {
30✔
465
            this.selectionModel = new SelectionModel(this.isMultiple);
20✔
466
        }
467
    }
10✔
468

469
    private initPosition() {
470
        this.cascaderPosition = EXPANDED_DROPDOWN_POSITIONS.map(item => {
4!
471
            return { ...item };
4✔
472
        });
UNCOV
473
        this.cascaderPosition[0].offsetY = 4; // 左下
×
474
        this.cascaderPosition[1].offsetY = 4; // 右下
475
        this.cascaderPosition[2].offsetY = -4; // 右下
476
        this.cascaderPosition[3].offsetY = -4; // 右下
2!
477
        this.positions = this.cascaderPosition;
2✔
478
    }
UNCOV
479

×
480
    private initOptions(index: number) {
481
        const vs = this.defaultValue;
482
        const load = () => {
32✔
483
            this.activateOnInit(index, vs[index]);
2✔
484
            if (index < vs.length - 1) {
485
                this.initOptions(index + 1);
30!
486
            }
30✔
487
            if (index === vs.length - 1) {
488
                this.afterWriteValue();
489
            }
490
        };
2!
UNCOV
491

×
492
        if (this.isLoaded(index) || !this.thyLoadData) {
493
            load();
2!
494
        } else {
2✔
495
            const node = this.activatedOptions[index - 1] || {};
496
            this.loadChildren(node, index - 1, load, this.afterWriteValue.bind(this));
497
        }
498
    }
20!
UNCOV
499

×
500
    private activateOnInit(index: number, value: any): void {
501
        let option = this.findOption(value, index);
20✔
502
        if (!option) {
20✔
503
            option =
1✔
504
                typeof value === 'object'
505
                    ? value
506
                    : {
19✔
507
                          [`${this.thyValueProperty || 'value'}`]: value,
508
                          [`${this.thyLabelProperty || 'label'}`]: value
509
                      };
510
        }
1✔
511
        this.updatePrevSelectedOptions(option, true);
1✔
512
        this.setActiveOption(option, index, false, false);
1✔
513
    }
3✔
514

3✔
515
    private updatePrevSelectedOptions(option: ThyCascaderOption, isActivateInit: boolean, index?: number) {
3✔
516
        if (isActivateInit) {
517
            if (this.thyIsOnlySelectLeaf && option.isLeaf) {
1✔
518
                set(option, 'selected', true);
3✔
519
            }
3✔
520
            this.prevSelectedOptions.add(option);
2✔
521
        } else {
522
            if (!this.thyMultiple) {
523
                const prevSelectedOptions = Array.from(this.prevSelectedOptions);
524
                while (prevSelectedOptions.length) {
525
                    set(prevSelectedOptions.pop(), 'selected', false);
1✔
526
                }
1!
527
                this.prevSelectedOptions = new Set([]);
1✔
528
            }
4✔
529
            if (this.thyIsOnlySelectLeaf && !option.isLeaf && this.thyMultiple) {
4✔
530
                set(option, 'selected', this.isSelectedOption(option, index));
3✔
531
            } else {
532
                set(option, 'selected', !this.isSelectedOption(option, index));
533
            }
534
            if (this.thyIsOnlySelectLeaf && this.thyMultiple && option.parent) {
535
                this.updatePrevSelectedOptions(option.parent, false, index - 1);
1!
UNCOV
536
            }
×
537
            this.prevSelectedOptions.add(option);
538
        }
539
    }
540

1✔
541
    writeValue(value: any): void {
542
        if (!this.selectionModel) {
543
            this.initSelectionModel();
544
        }
545
        if (!this.isMultiple) {
546
            const vs = (this.defaultValue = toArray(value));
547
            if (vs.length) {
548
                this.initOptions(0);
549
            } else {
3,282!
UNCOV
550
                this.value = vs;
×
551
                this.activatedOptions = [];
552
                this.afterWriteValue();
3,282✔
553
            }
3,577✔
554
        } else {
631✔
555
            const values = toArray(value);
556
            this.selectionModel.clear();
2,946✔
557
            values.forEach(item => {
860✔
558
                const vs = (this.defaultValue = toArray(item));
559
                if (vs.length) {
560
                    this.initOptions(0);
1,791✔
561
                } else {
562
                    this.value = vs;
563
                    this.activatedOptions = [];
2!
564
                    this.afterWriteValue();
2✔
565
                }
566
            });
2!
UNCOV
567
            this.cdr.detectChanges();
×
568
        }
569
    }
2!
570

1✔
571
    afterWriteValue(): void {
572
        this.selectedOptions = this.activatedOptions;
1✔
573
        this.value = this.getSubmitValue(this.selectedOptions);
574
        this.addSelectedState(this.selectedOptions);
575
        this.buildDisplayLabel();
2!
576
    }
2✔
577

578
    private addSelectedState(selectOptions: ThyCascaderOption[]) {
2✔
579
        if (this.isMultiple && this.thyIsOnlySelectLeaf) {
1✔
580
            selectOptions.forEach(opt => {
581
                if (opt.isLeaf) {
1✔
582
                    opt.selected = true;
583
                    set(opt, 'selected', true);
584
                }
585
            });
1!
UNCOV
586
            this.addParentSelectedState(selectOptions);
×
587
        }
588
    }
1✔
589

590
    addParentSelectedState(selectOptions: ThyCascaderOption[]) {
591
        selectOptions.forEach(opt => {
1!
592
            if (opt.children && opt.children.length && opt.children.every(i => i.selected)) {
1✔
593
                opt.selected = true;
1✔
594
                set(opt, 'selected', true);
595
                if (opt.parent) {
596
                    this.addParentSelectedState([opt.parent]);
597
                }
7✔
598
            }
1✔
599
        });
1✔
600
    }
1✔
601

1✔
602
    setDisabledState(isDisabled: boolean): void {
603
        this.disabled = isDisabled;
604
    }
26✔
605

138✔
606
    public positionChange(position: ConnectedOverlayPositionChange): void {
138✔
607
        const newValue = position.connectionPair.originY === 'bottom' ? 'bottom' : 'top';
122✔
608
        if (this.dropDownPosition !== newValue) {
122✔
609
            this.dropDownPosition = newValue;
3✔
610
            this.cdr.detectChanges();
611
        }
612
    }
138✔
613

20✔
614
    private isLoaded(index: number): boolean {
615
        return this.columns[index] && this.columns[index].length > 0;
138✔
616
    }
83✔
617

92✔
618
    public getOptionLabel(option: ThyCascaderOption): any {
83✔
619
        return option[this.thyLabelProperty || 'label'];
620
    }
55✔
621

3✔
622
    public getOptionValue(option: ThyCascaderOption): any {
623
        return option[this.thyValueProperty || 'value'];
624
    }
52!
UNCOV
625

×
626
    public isActivatedOption(option: ThyCascaderOption, index: number): boolean {
627
        if (!this.isMultiple || this.thyIsOnlySelectLeaf) {
628
            const activeOpt = this.activatedOptions[index];
138✔
629
            return activeOpt === option;
9✔
630
        } else {
631
            if (option.isLeaf) {
632
                return option.selected;
633
            } else {
9✔
634
                const selectedOpts = this.selectionModel.selected;
9✔
635
                const appearIndex = selectedOpts.findIndex(item => {
9!
636
                    const selectedItem = helpers.get(item, `thyRawValue.value.${index}`);
9✔
637
                    return helpers.shallowEqual(selectedItem, option);
9✔
638
                });
9✔
639
                return appearIndex >= 0;
8✔
640
            }
641
        }
642
    }
1✔
643

1✔
644
    public isHalfSelectedOption(option: ThyCascaderOption, index: number): boolean {
1!
645
        if (!option.selected && this.thyIsOnlySelectLeaf && !option.isLeaf && !this.checkSelectedStatus(option, false)) {
1✔
646
            return true;
1✔
647
        }
648
        return false;
UNCOV
649
    }
×
650

651
    public isSelectedOption(option: ThyCascaderOption, index: number): boolean {
652
        if (this.thyIsOnlySelectLeaf) {
1✔
653
            if (option.isLeaf) {
654
                return option.selected;
9✔
655
            } else {
656
                return this.checkSelectedStatus(option, true);
9✔
657
            }
4✔
658
        } else {
4✔
659
            const selectedOpts = this.selectionModel.selected;
660
            const appearIndex = selectedOpts.findIndex(item => {
661
                if (item.thyRawValue.value.length - 1 === index) {
662
                    const selectedItem = helpers.get(item, `thyRawValue.value.${index}`);
1✔
663
                    return helpers.shallowEqual(selectedItem, option);
1✔
664
                } else {
1✔
665
                    return false;
1✔
666
                }
667
            });
1✔
668
            return appearIndex >= 0;
1✔
669
        }
670
    }
1✔
671

1!
672
    public attached(): void {
1✔
673
        this.cdr.detectChanges();
674
        this.cdkConnectedOverlay.positionChange.pipe(take(1), takeUntil(this.destroy$)).subscribe(() => {
1✔
675
            this.scrollActiveElementIntoView();
676
        });
677
    }
2✔
678

2✔
679
    private scrollActiveElementIntoView() {
4✔
680
        if (!isEmpty(this.selectedOptions)) {
1✔
681
            const activeOptions = this.cascaderOptions
682
                .filter(item => item.nativeElement.classList.contains('thy-cascader-menu-item-active'))
683
                // for multiple mode
684
                .slice(-this.cascaderOptionContainers.length);
UNCOV
685

×
686
            this.cascaderOptionContainers.forEach((item, index) => {
687
                if (index <= activeOptions.length - 1) {
688
                    ScrollToService.scrollToElement(activeOptions[index].nativeElement, item.nativeElement);
9✔
689
                    this.cdr.detectChanges();
9!
690
                }
9✔
691
            });
9✔
692
        }
9✔
693
    }
9✔
694

1✔
695
    private findOption(option: any, index: number): ThyCascaderOption {
696
        const options: ThyCascaderOption[] = this.columns[index];
9✔
697
        if (options) {
9✔
698
            const value = typeof option === 'object' ? this.getOptionValue(option) : option;
699
            return options.find(o => value === this.getOptionValue(o));
700
        }
701
        return null;
702
    }
9✔
703

15✔
704
    private buildDisplayLabel(): void {
9✔
705
        const selectedOptions = [...this.selectedOptions];
706
        const labels: string[] = selectedOptions.map(o => this.getOptionLabel(o));
707
        if (labels.length === 0) {
1!
708
            return;
1✔
709
        }
1✔
710
        let labelRenderContext;
711
        let labelRenderText;
1✔
712
        if (this.isLabelRenderTemplate) {
1✔
713
            labelRenderContext = { labels, selectedOptions };
1✔
714
        } else {
1✔
715
            labelRenderText = defaultDisplayRender.call(this, labels, selectedOptions);
1✔
716
            this.labelRenderText = labelRenderText;
1✔
717
        }
1✔
718
        if (this.labelRenderText || this.isLabelRenderTemplate) {
719
            const selectedData: SelectOptionBase = {
720
                thyRawValue: {
1✔
721
                    value: selectedOptions,
1✔
722
                    labelText: labelRenderText,
1✔
723
                    labelRenderContext: labelRenderContext
724
                },
725
                thyValue: labels,
5✔
726
                thyLabelText: labelRenderText
4✔
727
            };
4✔
728
            this.selectionModel.select(selectedData);
3✔
729
        }
3!
730
    }
4✔
731

3✔
732
    public isMenuVisible(): boolean {
733
        return this.menuVisible;
3✔
734
    }
1✔
735

736
    public setMenuVisible(menuVisible: boolean): void {
737
        if (this.menuVisible !== menuVisible) {
1✔
738
            this.menuVisible = menuVisible;
1✔
739

1!
740
            this.initActivatedOptions();
1✔
741
            this.setClassMap();
742
            this.setMenuClass();
743
            if (this.menuVisible) {
744
                this.triggerRect = this.trigger.nativeElement.getBoundingClientRect();
745
            }
1!
746
            this.thyExpandStatusChange.emit(menuVisible);
747
        }
748
    }
749

89✔
750
    private initActivatedOptions() {
85✔
751
        if (isEmpty(this.selectedOptions) || !this.menuVisible) {
85✔
752
            return;
19✔
753
        }
754
        this.activatedOptions = [...this.selectedOptions];
755
        this.activatedOptions.forEach((item, index) => {
756
            if (!isEmpty(item.children) && !item.isLeaf) {
757
                this.columns[index + 1] = item.children;
121✔
758
            }
121!
759
        });
149✔
760
    }
761

121✔
762
    public get menuCls(): any {
763
        return this._menuCls;
764
    }
48✔
765

48✔
766
    private setMenuClass(): void {
48✔
767
        this._menuCls = {
48✔
768
            [`${this.prefixCls}-menus`]: true,
48✔
769
            [`${this.prefixCls}-menus-hidden`]: !this.menuVisible,
48✔
770
            [`${this.thyMenuClassName}`]: this.thyMenuClassName,
48✔
771
            [`w-100`]: this.columns.length === 0
48✔
772
        };
48✔
773
    }
48✔
774

48✔
775
    public get menuColumnCls(): any {
48✔
776
        return this._menuColumnCls;
48✔
777
    }
48✔
778

48✔
779
    private setMenuColumnClass(): void {
48✔
780
        this._menuColumnCls = {
48✔
781
            [`${this.prefixCls}-menu`]: true,
48✔
782
            [`${this.thyColumnClassName}`]: this.thyColumnClassName
48✔
783
        };
48✔
784
    }
48✔
785

48✔
786
    public get labelCls(): any {
48✔
787
        return this._labelCls;
48✔
788
    }
48✔
789

48✔
790
    private setLabelClass(): void {
48✔
791
        this._labelCls = {
48✔
792
            [`${this.prefixCls}-picker-label`]: true,
48✔
793
            [`${this.prefixCls}-show-search`]: false,
48✔
794
            [`${this.prefixCls}-focused`]: false
48✔
795
        };
48✔
796
    }
48✔
797

48✔
798
    private setClassMap(): void {
48✔
799
        const classMap = {
48✔
800
            [`${this.prefixCls}`]: true,
48✔
801
            [`${this.prefixCls}-picker`]: true,
48✔
802
            [`${this.prefixCls}-${this.thySize}`]: true,
48✔
803
            [`${this.prefixCls}-picker-disabled`]: this.disabled,
48✔
804
            [`${this.prefixCls}-picker-open`]: this.menuVisible
48✔
805
        };
48✔
806
        this.hostRenderer.updateClassByMap(classMap);
48✔
807
    }
48✔
808

48✔
809
    private isClickTriggerAction(): boolean {
810
        if (typeof this.thyTriggerAction === 'string') {
811
            return this.thyTriggerAction === 'click';
812
        }
813
        return this.thyTriggerAction.indexOf('click') !== -1;
814
    }
815

48✔
816
    private isHoverTriggerAction(): boolean {
48✔
817
        if (typeof this.thyTriggerAction === 'string') {
48✔
818
            return this.thyTriggerAction === 'hover';
48✔
819
        }
820
        return this.thyTriggerAction.indexOf('hover') !== -1;
821
    }
917!
822

823
    private isHoverExpandTriggerAction(): boolean {
824
        if (typeof this.thyExpandTriggerAction === 'string') {
7✔
825
            return this.thyExpandTriggerAction === 'hover';
1✔
826
        }
827
        return this.thyExpandTriggerAction.indexOf('hover') !== -1;
7✔
828
    }
829

830
    @HostListener('click', ['$event'])
48✔
831
    public toggleClick($event: Event) {
49✔
832
        if (this.disabled) {
833
            return;
6✔
834
        }
835
        if (this.isClickTriggerAction()) {
6✔
836
            this.setMenuVisible(!this.menuVisible);
6✔
837
        }
838
    }
839

18✔
840
    @HostListener('mouseover', ['$event'])
38✔
841
    public toggleHover($event: Event) {
57✔
842
        if (this.disabled) {
57✔
843
            return;
57✔
844
        }
57✔
845
        if (this.isHoverTriggerAction()) {
57✔
846
            this.setMenuVisible(!this.menuVisible);
57✔
847
        }
57✔
848
    }
849

850
    public clickOption(option: ThyCascaderOption, index: number, event: Event | boolean): void {
851
        if (option && option.disabled && !this.isMultiple) {
852
            return;
853
        }
854
        const isSelect = event instanceof Event ? !this.isMultiple && option.isLeaf : true;
855
        if (this.isMultiple && !option.isLeaf && this.thyIsOnlySelectLeaf && isSelect) {
57✔
856
            this.toggleAllChildren(option, index, event as boolean);
32✔
857
        } else {
858
            this.setActiveOption(option, index, isSelect);
859
        }
25✔
860
    }
861

862
    private toggleAllChildren(option: ThyCascaderOption, index: number, selected: boolean): void {
863
        const allLeafs: {
864
            option: ThyCascaderOption;
865
            index: number;
866
        }[] = this.getAllLeafs(option, index, selected);
867
        option.selected = selected;
868
        while (allLeafs.length) {
869
            const { option, index } = allLeafs.shift();
870
            option.selected = !selected;
871
            this.setActiveOption(option, index, true);
6✔
872
        }
6✔
873
        for (let i = 0; i < this.activatedOptions.length; i++) {
29✔
874
            const option = this.activatedOptions[i];
4✔
875
            if (isArray(option.children) && option.children.length) {
876
                this.setColumnData(option.children, i + 1);
877
            }
878
        }
879
    }
6✔
880

6✔
881
    private getAllLeafs(
882
        option: ThyCascaderOption,
883
        index: number,
8✔
884
        selected: boolean
8✔
885
    ): {
8✔
886
        option: ThyCascaderOption;
8✔
887
        index: number;
8✔
888
    }[] {
889
        let allLeafs: {
890
            option: ThyCascaderOption;
1✔
891
            index: number;
1!
UNCOV
892
        }[] = [];
×
UNCOV
893
        if (option.children.length > 0) {
×
894
            for (const childOption of option.children) {
UNCOV
895
                childOption.parent = option;
×
896
                if (childOption.isLeaf && !childOption.selected === selected) {
897
                    allLeafs.push({
1!
UNCOV
898
                        option: childOption,
×
UNCOV
899
                        index: index + 1
×
UNCOV
900
                    });
×
901
                } else if (!childOption.isLeaf) {
×
902
                    allLeafs = allLeafs.concat(this.getAllLeafs(childOption, index + 1, selected));
UNCOV
903
                }
×
904
            }
UNCOV
905
        }
×
UNCOV
906
        return allLeafs;
×
907
    }
×
908

×
909
    /**
910
     * 检查所有所有子项的选择状态, 有一个不符合预期,就直接返回 false
911
     * @param option
912
     * @param trueOrFalse
1✔
913
     * @private
3✔
914
     */
915
    private checkSelectedStatus(option: ThyCascaderOption, isSelected: boolean): boolean {
1✔
916
        if (option.isLeaf) {
917
            return option.selected === isSelected;
918
        }
919
        for (const childOption of option.children) {
48✔
920
            if (isArray(childOption.children) && childOption.children.length && !this.checkSelectedStatus(childOption, isSelected)) {
48✔
921
                return false;
922
            }
1✔
923
            if (!childOption.selected === isSelected) {
924
                return false;
925
            }
926
        }
927
        return true;
928
    }
929

930
    public mouseoverOption(option: ThyCascaderOption, index: number, event: Event): void {
1✔
931
        if (event) {
932
            event.preventDefault();
933
        }
934

935
        if (option && option.disabled && !this.isMultiple) {
936
            return;
937
        }
938

939
        if (!this.isHoverExpandTriggerAction() && !(option && option.disabled && this.isMultiple)) {
940
            return;
941
        }
942
        this.setActiveOption(option, index, false);
943
    }
944

945
    public mouseleaveMenu(event: Event) {
946
        if (event) {
947
            event.preventDefault();
948
        }
949
        if (!this.isHoverTriggerAction()) {
950
            return;
951
        }
952
        this.setMenuVisible(!this.menuVisible);
953
    }
954

955
    onBlur(event?: FocusEvent) {
956
        // Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
957
        if (elementMatchClosest(event?.relatedTarget as HTMLElement, ['.thy-cascader-menus', 'thy-cascader'])) {
958
            return;
959
        }
960
        this.onTouchedFn();
961
    }
962

963
    onFocus(event?: FocusEvent) {
964
        if (!elementMatchClosest(event?.relatedTarget as HTMLElement, ['.thy-cascader-menus', 'thy-cascader'])) {
965
            const inputElement: HTMLInputElement = this.elementRef.nativeElement.querySelector('input');
966
            inputElement.focus();
967
        }
968
    }
969

1✔
970
    public closeMenu(): void {
971
        if (this.menuVisible) {
972
            this.setMenuVisible(false);
973
            this.onTouchedFn();
1✔
974
            this.isShowSearchPanel = false;
975
            this.searchResultList = [];
976
        }
977
    }
1✔
978

979
    public setActiveOption(option: ThyCascaderOption, index: number, select: boolean, loadChildren: boolean = true): void {
980
        this.activatedOptions[index] = option;
981
        for (let i = index - 1; i >= 0; i--) {
982
            const originOption = this.activatedOptions[i + 1]?.parent;
1✔
983
            if (!this.activatedOptions[i] || originOption?.[this.thyValueProperty] !== this.activatedOptions[i]?.[this.thyValueProperty]) {
984
                this.activatedOptions[i] = originOption ?? this.activatedOptions[i];
985
            }
986
        }
1✔
987
        if (index < this.activatedOptions.length - 1) {
988
            this.activatedOptions = this.activatedOptions.slice(0, index + 1);
989
        }
990
        if (isArray(option.children) && option.children.length) {
1✔
991
            option.isLeaf = false;
992
            option.children.forEach(child => (child.parent = option));
993
            this.setColumnData(option.children, index + 1);
994
        } else if (!option.isLeaf && loadChildren) {
1✔
995
            this.loadChildren(option, index);
996
        } else {
997
            if (index < this.columns.length - 1) {
998
                this.columns = this.columns.slice(0, index + 1);
999
            }
1000
        }
1001
        if (select) {
48✔
1002
            this.selectOption(option, index);
1003
        }
1004
    }
1005

1006
    private selectOption(option: ThyCascaderOption, index: number): void {
1007
        this.thySelect.emit({ option, index });
1008
        const isOptionCanSelect = this.thyChangeOnSelect && !this.isMultiple;
1009
        if (option.isLeaf || !this.thyIsOnlySelectLeaf || isOptionCanSelect || this.shouldPerformSelection(option, index)) {
1010
            this.selectedOptions = this.activatedOptions;
1011
            this.updatePrevSelectedOptions(option, false, index);
1012
            if (option.selected) {
1013
                this.buildDisplayLabel();
1014
            } else {
1015
                const selectedItems = this.selectionModel.selected;
1016
                const currentItem = selectedItems.find(item => {
1017
                    if (item.thyRawValue.value.length - 1 === index) {
1018
                        const selectedItem = helpers.get(item, `thyRawValue.value.${index}`);
1019
                        return helpers.shallowEqual(selectedItem, option);
1020
                    } else {
1021
                        return false;
1022
                    }
1023
                });
1024
                this.selectionModel.deselect(currentItem);
1025
            }
1026
            this.valueChange$.next();
1027
        }
1028
        if ((option.isLeaf || !this.thyIsOnlySelectLeaf) && !this.thyMultiple) {
1029
            this.setMenuVisible(false);
1030
            this.onTouchedFn();
1031
        }
1032
    }
1033

1034
    public removeSelectedItem(event: { item: SelectOptionBase; $eventOrigin: Event }) {
1035
        const selectedItems = this.selectionModel.selected;
1036
        event.$eventOrigin.stopPropagation();
1037
        const currentItem = selectedItems.find(item => {
1038
            return helpers.shallowEqual(item.thyValue, event.item.thyValue);
1039
        });
1040
        this.deselectOption(currentItem);
1041
        this.selectionModel.deselect(currentItem);
1042
        // update selectedOptions
1043
        const updatedSelectedItems = this.selectionModel.selected;
1044
        if (isArray(updatedSelectedItems) && updatedSelectedItems.length) {
1045
            this.selectedOptions = updatedSelectedItems[updatedSelectedItems.length - 1].thyRawValue.value;
1046
        }
1047
        this.valueChange$.next();
1048
    }
1049

1050
    private deselectOption(option: SelectOptionBase) {
1051
        const value: ThyCascaderOption[] = option.thyRawValue.value;
1052
        value.forEach(item => {
1053
            if (item.isLeaf && item.selected) {
1054
                set(item, 'selected', false);
1055
            }
1056
        });
1057
    }
1058

1059
    private shouldPerformSelection(option: ThyCascaderOption, level: number): boolean {
1060
        return typeof this.thyChangeOn === 'function' ? this.thyChangeOn(option, level) === true : false;
1061
    }
1062

1063
    private valueChange(): void {
1064
        const value = this.getValues();
1065
        if (!arrayEquals(this.value, value)) {
1066
            this.defaultValue = null;
1067
            this.value = value;
1068
            this.onChangeFn(value);
1069
            if (this.selectionModel.isEmpty()) {
1070
                this.thyClear.emit();
1071
            }
1072
            this.thySelectionChange.emit(this.selectedOptions);
1073
            this.thyChange.emit(value);
1074
        }
1075
    }
1076

1077
    private getValues() {
1078
        let selectedItems: any[];
1079
        const selected = this.selectionModel.selected;
1080
        selectedItems = selected.map(item => this.getSubmitValue(item.thyRawValue.value));
1081
        return this.isMultiple ? selectedItems : selectedItems[0] ?? selectedItems;
1082
    }
1083

1084
    public clearSelection($event: Event): void {
1085
        if ($event) {
1086
            $event.stopPropagation();
1087
            $event.preventDefault();
1088
        }
1089
        this.labelRenderText = '';
1090
        this.labelRenderContext = {};
1091
        this.selectedOptions = [];
1092
        this.activatedOptions = [];
1093
        this.deselectAllSelected();
1094
        this.setMenuVisible(false);
1095
        this.valueChange$.next();
1096
    }
1097

1098
    private deselectAllSelected() {
1099
        const selectedOptions = this.selectionModel.selected;
1100
        selectedOptions.forEach(item => this.deselectOption(item));
1101
        this.selectionModel.clear();
1102
    }
1103

1104
    private loadChildren(option: ThyCascaderOption, index: number, success?: () => void, failure?: () => void): void {
1105
        if (this.thyLoadData) {
1106
            this.isLoading = true;
1107
            this.thyLoadData(option, index).then(
1108
                () => {
1109
                    option.loading = this.isLoading = false;
1110
                    if (option.children) {
1111
                        option.children.forEach(child => (child.parent = index < 0 ? undefined : option));
1112
                        this.setColumnData(option.children, index + 1);
1113
                    }
1114
                    if (success) {
1115
                        success();
1116
                    }
1117
                },
1118
                () => {
1119
                    option.loading = this.isLoading = false;
1120
                    option.isLeaf = true;
1121
                    if (failure) {
1122
                        failure();
1123
                    }
1124
                }
1125
            );
1126
        } else {
1127
            this.setColumnData(option.children || [], index + 1);
1128
        }
1129
    }
1130

1131
    private setColumnData(options: ThyCascaderOption[], index: number): void {
1132
        if (!arrayEquals(this.columns[index], options)) {
1133
            this.columns[index] = options;
1134
            if (index < this.columns.length - 1) {
1135
                this.columns = this.columns.slice(0, index + 1);
1136
            }
1137
        }
1138
    }
1139

1140
    private getSubmitValue(originOptions: ThyCascaderOption[]): any[] {
1141
        const values: any[] = [];
1142
        (originOptions || []).forEach(option => {
1143
            values.push(this.getOptionValue(option));
1144
        });
1145
        return values;
1146
    }
1147

1148
    constructor(
1149
        @Inject(PLATFORM_ID) private platformId: string,
1150
        private cdr: ChangeDetectorRef,
1151
        private viewPortRuler: ViewportRuler,
1152
        public elementRef: ElementRef,
1153
        private thyClickDispatcher: ThyClickDispatcher,
1154
        private ngZone: NgZone
1155
    ) {
1156
        super();
1157
    }
1158

1159
    public trackByFn(index: number, item: ThyCascaderOption) {
1160
        return item?.value || item?._id || index;
1161
    }
1162

1163
    public searchFilter(searchText: string) {
1164
        if (!searchText && !this.isSelectingSearchState) {
1165
            this.resetSearch();
1166
        }
1167
        this.searchText$.next(searchText);
1168
    }
1169

1170
    private initSearch() {
1171
        this.searchText$
1172
            .pipe(
1173
                takeUntil(this.destroy$),
1174
                debounceTime(200),
1175
                distinctUntilChanged(),
1176
                filter(text => text !== '')
1177
            )
1178
            .subscribe(searchText => {
1179
                this.resetSearch();
1180

1181
                // local search
1182
                this.searchInLocal(searchText);
1183
                this.isShowSearchPanel = true;
1184
            });
1185
    }
1186

1187
    private forEachColumns(
1188
        currentLabel?: string[],
1189
        currentValue: Id[] = [],
1190
        currentRowValue: ThyCascaderOption[] = [],
1191
        list = this.columns[0]
1192
    ) {
1193
        list.forEach(item => {
1194
            const curOptionLabel = this.getOptionLabel(item);
1195
            const curOptionValue = this.getOptionValue(item);
1196
            const label: string[] = currentLabel ? [...currentLabel, curOptionLabel] : [curOptionLabel];
1197
            const valueList: Id[] = [...currentValue, curOptionValue];
1198
            const rowValueList: ThyCascaderOption[] = [...currentRowValue, item];
1199
            const isSelected = this.isSelectedOption(item, valueList.length - 1);
1200

1201
            this.flattenOptions.push({
1202
                labelList: label,
1203
                valueList,
1204
                selected: isSelected,
1205
                thyRowValue: rowValueList,
1206
                isLeaf: item.isLeaf,
1207
                disabled: item.disabled
1208
            });
1209
            if (item.children && item.children.length) {
1210
                this.forEachColumns(label, valueList, rowValueList, item.children);
1211
            } else {
1212
                this.leafNodes.push({
1213
                    labelList: label,
1214
                    valueList,
1215
                    selected: isSelected,
1216
                    thyRowValue: rowValueList,
1217
                    isLeaf: item.isLeaf,
1218
                    disabled: item.disabled
1219
                });
1220
            }
1221
        });
1222
    }
1223

1224
    private setSearchResultList(listOfOption: ThyCascaderSearchOption[], searchText: string) {
1225
        this.searchResultList = [];
1226
        listOfOption.forEach(item => {
1227
            if (!item.disabled && item.isLeaf && item.labelList.join().toLowerCase().indexOf(searchText.toLowerCase()) !== -1) {
1228
                this.searchResultList.push(item);
1229
            }
1230
        });
1231
    }
1232

1233
    private searchInLocal(searchText: string): void {
1234
        this.forEachColumns();
1235

1236
        this.setSearchResultList(this.thyIsOnlySelectLeaf ? this.leafNodes : this.flattenOptions, searchText);
1237
    }
1238

1239
    private resetSearch() {
1240
        this.isShowSearchPanel = false;
1241
        this.searchResultList = [];
1242
        this.leafNodes = [];
1243
        this.flattenOptions = [];
1244
        this.scrollActiveElementIntoView();
1245
    }
1246

1247
    public selectSearchResult(selectOptionData: ThyCascaderSearchOption): void {
1248
        const { thyRowValue: selectedOptions } = selectOptionData;
1249
        if (selectOptionData.selected) {
1250
            if (!this.isMultiple) {
1251
                this.closeMenu();
1252
            }
1253
            return;
1254
        }
1255
        if (this.isMultiple) {
1256
            this.isSelectingSearchState = true;
1257
            selectOptionData.selected = true;
1258
            selectedOptions.forEach((item: ThyCascaderOption, index: number) => {
1259
                this.setActiveOption(item, index, index === selectedOptions.length - 1);
1260
            });
1261
            const originSearchResultList = this.searchResultList;
1262
            // 保持搜索选项
1263
            setTimeout(() => {
1264
                this.isShowSearchPanel = true;
1265
                this.searchResultList = originSearchResultList;
1266
                this.isSelectingSearchState = false;
1267
            });
1268
        } else {
1269
            selectedOptions.forEach((item: ThyCascaderOption, index: number) => {
1270
                this.setActiveOption(item, index, index === selectedOptions.length - 1);
1271
            });
1272

1273
            this.resetSearch();
1274
        }
1275
    }
1276

1277
    ngOnDestroy() {
1278
        this.destroy$.next();
1279
        this.destroy$.complete();
1280
    }
1281
}
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