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

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

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

8.14
/src/mention/mention.directive.ts
1
import { ThyPopover, ThyPopoverConfig, ThyPopoverRef } from 'ngx-tethys/popover';
2
import { EMPTY, fromEvent, Subject } from 'rxjs';
3
import { switchMap, takeUntil } from 'rxjs/operators';
4

5
import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, computed, inject, input, output } from '@angular/core';
6
import { NgControl } from '@angular/forms';
7

8
import { createMentionAdapter, MatchedMention, MentionAdapter, MentionInputorElement } from './adapter';
9
import { CaretPositioner } from './caret-positioner';
10
import { Mention, MentionDefaultDataItem, MentionSuggestionSelectEvent } from './interfaces';
11
import { ThyMentionSuggestions } from './suggestions/suggestions.component';
12
import { isInputOrTextarea } from 'ngx-tethys/util';
1✔
13

1✔
14
const SUGGESTION_BACKDROP_CLASS = 'thy-mention-suggestions-backdrop';
1✔
15

16
const POPOVER_DEFAULT_CONFIG = { backdropClass: SUGGESTION_BACKDROP_CLASS, placement: 'bottomLeft' };
17

UNCOV
18
const DEFAULT_MENTION_CONFIG: Partial<Mention> = {
×
UNCOV
19
    autoClose: true,
×
20
    emptyText: '无匹配数据,按空格完成输入',
21
    search: (term: string, data: MentionDefaultDataItem[]) => {
22
        return data.filter(item => {
23
            return !item.name || item.name.toLowerCase().includes(term.toLowerCase());
24
        });
25
    }
26
};
27

1✔
28
/**
UNCOV
29
 * @name thyMention
×
30
 * @order 10
31
 */
UNCOV
32
@Directive({ selector: '[thyMention]' })
×
UNCOV
33
export class ThyMentionDirective implements OnInit, OnDestroy {
×
UNCOV
34
    private elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
×
UNCOV
35
    private thyPopover = inject(ThyPopover);
×
UNCOV
36
    private ngControl = inject(NgControl, { optional: true, self: true })!;
×
UNCOV
37

×
UNCOV
38
    private adapter: MentionAdapter = null;
×
UNCOV
39

×
UNCOV
40
    public openedSuggestionsRef: ThyPopoverRef<ThyMentionSuggestions>;
×
UNCOV
41

×
UNCOV
42
    /**
×
43
     * 提及输入配置参数,同时支持多个提及规则
×
44
     * @type Mention<any>[]
UNCOV
45
     */
×
46
    readonly thyMention = input<Mention<any>[]>([]);
47

48
    /**
49
     * Popover 弹出层参数配置
×
50
     */
51
    readonly thyPopoverConfig = input<ThyPopoverConfig>();
UNCOV
52

×
UNCOV
53
    /**
×
UNCOV
54
     * 选择后的回调函数
×
UNCOV
55
     */
×
56
    readonly thySelectSuggestion = output<MentionSuggestionSelectEvent>();
57

UNCOV
58
    get isOpened() {
×
59
        return !!this.openedSuggestionsRef;
UNCOV
60
    }
×
UNCOV
61

×
62
    mentions = computed(() => {
UNCOV
63
        if (this.thyMention()) {
×
UNCOV
64
            return this.thyMention().map(mention => {
×
65
                if ((typeof ngDevMode === 'undefined' || ngDevMode) && !mention.trigger) {
66
                    throw new Error(`mention trigger is required`);
67
                }
UNCOV
68
                return Object.assign({}, DEFAULT_MENTION_CONFIG, mention);
×
69
            });
UNCOV
70
        } else {
×
UNCOV
71
            return [];
×
UNCOV
72
        }
×
UNCOV
73
    });
×
74

75
    private destroy$ = new Subject<void>();
76

UNCOV
77
    private openedSuggestionsRef$ = new Subject<ThyPopoverRef<ThyMentionSuggestions> | null>();
×
78

UNCOV
79
    constructor() {
×
UNCOV
80
        const elementRef = this.elementRef;
×
81

82
        this.adapter = createMentionAdapter(elementRef.nativeElement as MentionInputorElement);
83
    }
UNCOV
84

×
85
    ngOnInit() {
86
        fromEvent(this.elementRef.nativeElement, 'input')
UNCOV
87
            .pipe(takeUntil(this.destroy$))
×
88
            .subscribe(event => this.onInput(event));
89

UNCOV
90
        fromEvent(this.elementRef.nativeElement, 'click')
×
91
            .pipe(takeUntil(this.destroy$))
92
            .subscribe(event => this.onClick(event));
UNCOV
93

×
UNCOV
94
        this.openedSuggestionsRef$
×
UNCOV
95
            .pipe(
×
96
                switchMap(openedSuggestionsRef =>
97
                    // Re-subscribe to `suggestionSelect$` every time the suggestions component is re-created,
UNCOV
98
                    // otherwise, unsubscribe, if it gets closed.
×
99
                    openedSuggestionsRef ? openedSuggestionsRef.componentInstance.suggestionSelect$ : EMPTY
100
                ),
101
                takeUntil(this.destroy$)
UNCOV
102
            )
×
UNCOV
103
            .subscribe(event => {
×
UNCOV
104
                const newValue = this.adapter.insertMention(event.item);
×
UNCOV
105
                if (isInputOrTextarea(this.elementRef.nativeElement)) {
×
UNCOV
106
                    if (this.ngControl && this.ngControl.control) {
×
107
                        this.ngControl.control.setValue(newValue);
108
                    }
109
                } else {
110
                    this.elementRef.nativeElement.innerText = newValue;
UNCOV
111
                }
×
UNCOV
112
                this.openedSuggestionsRef.close();
×
UNCOV
113
                this.thySelectSuggestion.emit(event);
×
114
            });
UNCOV
115
    }
×
116

UNCOV
117
    ngOnDestroy(): void {
×
UNCOV
118
        this.destroy$.next();
×
119
    }
120

121
    onClick(event: Event) {
UNCOV
122
        this.lookup(event);
×
UNCOV
123
    }
×
124

125
    onInput(event: Event) {
126
        this.lookup(event);
1✔
127
    }
1✔
128

129
    private lookup(event: Event) {
130
        const matched = this.adapter.lookup(event, this.mentions());
131
        if (matched) {
132
            this.openSuggestions(matched);
133
        } else {
1✔
134
            this.closeSuggestions();
135
        }
136
    }
137

138
    private openSuggestions(matched: MatchedMention) {
139
        if (!this.openedSuggestionsRef) {
140
            const inputElement = this.elementRef.nativeElement as HTMLInputElement;
141
            const position = CaretPositioner.getCaretPosition(inputElement, matched.query.start);
142
            const fontSize = parseInt(getComputedStyle(this.elementRef.nativeElement).fontSize, 10);
143
            this.openedSuggestionsRef = this.thyPopover.open(
144
                ThyMentionSuggestions,
145
                Object.assign(
146
                    {},
147
                    POPOVER_DEFAULT_CONFIG,
148
                    {
149
                        origin: this.elementRef,
150
                        originPosition: { x: position.left, y: position.top, width: fontSize, height: fontSize },
151
                        initialState: { mention: matched.mention }
152
                    },
153
                    this.thyPopoverConfig()
154
                )
155
            );
156
            this.openedSuggestionsRef.afterClosed().subscribe(() => {
157
                this.openedSuggestionsRef = null;
158
                this.openedSuggestionsRef$.next(null);
159
            });
160

161
            this.openedSuggestionsRef$.next(this.openedSuggestionsRef);
162
        }
163

164
        if (this.openedSuggestionsRef) {
165
            this.openedSuggestionsRef.componentInstance.search(matched.query);
166
        }
167
    }
168

169
    private closeSuggestions() {
170
        if (this.openedSuggestionsRef) {
171
            this.openedSuggestionsRef.close();
172
        }
173
    }
174
}
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