• 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

3.27
/src/anchor/anchor.component.ts
1
import { Platform } from '@angular/cdk/platform';
2
import {
3
    AfterViewInit,
4
    ChangeDetectionStrategy,
5
    ChangeDetectorRef,
6
    Component,
7
    ElementRef,
8
    NgZone,
9
    OnDestroy,
10
    Renderer2,
11
    ViewEncapsulation,
12
    numberAttribute,
1✔
13
    inject,
14
    input,
15
    viewChild,
16
    output,
17
    effect,
1✔
18
    computed,
UNCOV
19
    Signal
×
20
} from '@angular/core';
21
import { Subject, fromEvent } from 'rxjs';
UNCOV
22
import { takeUntil, throttleTime } from 'rxjs/operators';
×
23

24
import { DOCUMENT, NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
UNCOV
25
import { ThyAffix } from 'ngx-tethys/affix';
×
UNCOV
26
import { ThyScrollService } from 'ngx-tethys/core';
×
UNCOV
27
import { coerceBooleanProperty, getOffset } from 'ngx-tethys/util';
×
UNCOV
28
import { ThyAnchorLink } from './anchor-link.component';
×
UNCOV
29
import { IThyAnchorComponent, THY_ANCHOR_COMPONENT } from './anchor.token';
×
UNCOV
30

×
UNCOV
31
interface Section {
×
UNCOV
32
    linkComponent: ThyAnchorLink;
×
UNCOV
33
    top: number;
×
UNCOV
34
}
×
UNCOV
35

×
UNCOV
36
const sharpMatcherRegx = /#([^#]+)$/;
×
UNCOV
37

×
UNCOV
38
/**
×
UNCOV
39
 * 锚点组件
×
UNCOV
40
 * @name thy-anchor
×
UNCOV
41
 */
×
42
@Component({
×
43
    selector: 'thy-anchor',
44
    exportAs: 'thyAnchor',
UNCOV
45
    preserveWhitespaces: false,
×
UNCOV
46
    template: `
×
47
        @if (thyAffix()) {
48
            <thy-affix [thyOffsetTop]="thyOffsetTop()" [thyContainer]="container()">
49
                <ng-template [ngTemplateOutlet]="content"></ng-template>
UNCOV
50
            </thy-affix>
×
UNCOV
51
        } @else {
×
UNCOV
52
            <ng-template [ngTemplateOutlet]="content"></ng-template>
×
UNCOV
53
        }
×
UNCOV
54
        <ng-template #content>
×
UNCOV
55
            <div
×
UNCOV
56
                class="thy-anchor-wrapper"
×
57
                [ngClass]="{ 'thy-anchor-wrapper-horizontal': thyDirection() === 'horizontal' }"
58
                [ngStyle]="wrapperStyle()">
59
                <div class="thy-anchor">
60
                    <div class="thy-anchor-ink">
UNCOV
61
                        <div class="thy-anchor-ink-full" #ink></div>
×
UNCOV
62
                    </div>
×
63
                    <ng-content></ng-content>
64
                </div>
UNCOV
65
            </div>
×
UNCOV
66
        </ng-template>
×
UNCOV
67
    `,
×
68
    encapsulation: ViewEncapsulation.None,
69
    changeDetection: ChangeDetectionStrategy.OnPush,
UNCOV
70
    imports: [ThyAffix, NgTemplateOutlet, NgStyle, NgClass],
×
UNCOV
71
    providers: [
×
UNCOV
72
        {
×
UNCOV
73
            provide: THY_ANCHOR_COMPONENT,
×
74
            useExisting: ThyAnchor
75
        }
76
    ]
77
})
UNCOV
78
export class ThyAnchor implements IThyAnchorComponent, OnDestroy, AfterViewInit {
×
79
    private document = inject(DOCUMENT);
×
80
    private cdr = inject(ChangeDetectorRef);
UNCOV
81
    private platform = inject(Platform);
×
UNCOV
82
    private zone = inject(NgZone);
×
UNCOV
83
    private renderer = inject(Renderer2);
×
84
    private scrollService = inject(ThyScrollService);
UNCOV
85

×
86
    readonly ink = viewChild.required<ElementRef>('ink');
87

88
    /**
UNCOV
89
     * 固定模式
×
90
     */
91
    readonly thyAffix = input(true, { transform: coerceBooleanProperty });
UNCOV
92

×
UNCOV
93
    /**
×
94
     * 锚点区域边界,单位:px
UNCOV
95
     */
×
UNCOV
96
    readonly thyBounds = input(5, { transform: numberAttribute });
×
UNCOV
97

×
UNCOV
98
    /**
×
UNCOV
99
     * 缓冲的偏移量阈值
×
UNCOV
100
     */
×
101
    readonly thyOffsetTop = input<number, unknown>(undefined, { transform: numberAttribute });
×
102

UNCOV
103
    /**
×
UNCOV
104
     * 指定滚动的容器
×
UNCOV
105
     */
×
UNCOV
106
    readonly thyContainer = input<string | HTMLElement>(undefined);
×
UNCOV
107

×
108
    /**
109
     * 设置导航方向
110
     */
111
    readonly thyDirection = input<'vertical' | 'horizontal'>('vertical');
112

113
    /**
UNCOV
114
     * 点击项触发
×
UNCOV
115
     */
×
116
    readonly thyClick = output<ThyAnchorLink>();
×
117

×
118
    /**
119
     * 滚动到某锚点时触发
UNCOV
120
     */
×
UNCOV
121
    readonly thyScroll = output<ThyAnchorLink>();
×
122

UNCOV
123
    visible = false;
×
124

125
    readonly wrapperStyle = computed(() => {
UNCOV
126
        return {
×
UNCOV
127
            'max-height': this.thyOffsetTop() ? `calc(100vh - ${this.thyOffsetTop()}px)` : '100vh'
×
128
        };
129
    });
130

UNCOV
131
    readonly container: Signal<HTMLElement | Window> = computed(() => {
×
UNCOV
132
        return (
×
UNCOV
133
            (typeof this.thyContainer() === 'string'
×
UNCOV
134
                ? (this.document.querySelector(this.thyContainer() as string) as HTMLElement)
×
UNCOV
135
                : (this.thyContainer() as HTMLElement)) || window
×
UNCOV
136
        );
×
UNCOV
137
    });
×
UNCOV
138

×
UNCOV
139
    private links: ThyAnchorLink[] = [];
×
UNCOV
140

×
UNCOV
141
    private animating = false;
×
UNCOV
142

×
143
    private destroy$ = new Subject<void>();
144

UNCOV
145
    private handleScrollTimeoutID: any = -1;
×
UNCOV
146

×
UNCOV
147
    registerLink(link: ThyAnchorLink): void {
×
UNCOV
148
        this.links.push(link);
×
UNCOV
149
    }
×
UNCOV
150

×
151
    unregisterLink(link: ThyAnchorLink): void {
152
        this.links.splice(this.links.indexOf(link), 1);
153
    }
×
154

155
    constructor() {
156
        effect(() => {
157
            if (this.thyContainer()) {
UNCOV
158
                this.registerScrollEvent();
×
UNCOV
159
            }
×
UNCOV
160
        });
×
UNCOV
161
    }
×
162

UNCOV
163
    ngAfterViewInit(): void {
×
UNCOV
164
        this.warningPrompt();
×
UNCOV
165
        this.registerScrollEvent();
×
UNCOV
166
    }
×
UNCOV
167

×
UNCOV
168
    ngOnDestroy(): void {
×
169
        clearTimeout(this.handleScrollTimeoutID);
UNCOV
170
        this.destroy$.next();
×
UNCOV
171
        this.destroy$.complete();
×
172
    }
173

1✔
174
    private warningPrompt() {
1✔
175
        if (this.thyDirection() === 'horizontal') {
176
            const hasChildren = this.links.some(link =>
177
                Array.from(link?.elementRef?.nativeElement?.childNodes)?.some((item: HTMLElement) => item?.nodeName === 'THY-ANCHOR-LINK')
178
            );
179
            if (hasChildren) {
180
                console.warn("Anchor link nesting is not supported when 'Anchor' direction is horizontal.");
181
            }
182
        }
183
    }
184

185
    private registerScrollEvent(): void {
1✔
186
        if (!this.platform.isBrowser) {
187
            return;
188
        }
189
        this.destroy$.next();
190
        this.zone.runOutsideAngular(() => {
191
            fromEvent(this.container(), 'scroll', { passive: true })
192
                .pipe(throttleTime(50), takeUntil(this.destroy$))
193
                .subscribe(() => this.handleScroll());
194
        });
195
        // Browser would maintain the scrolling position when refreshing.
196
        // So we have to delay calculation in avoid of getting a incorrect result.
197
        this.handleScrollTimeoutID = setTimeout(() => this.handleScroll());
198
    }
199

200
    handleScroll(): void {
201
        if (typeof document === 'undefined' || this.animating) {
202
            return;
203
        }
204
        const container: HTMLElement =
205
            this.container() instanceof HTMLElement ? (this.container() as HTMLElement) : (this.document as unknown as HTMLElement);
206

207
        const sections: Section[] = [];
208
        const scope = (this.thyOffsetTop() || 0) + this.thyBounds();
209
        this.links.forEach(linkComponent => {
210
            const sharpLinkMatch = sharpMatcherRegx.exec(linkComponent.thyHref().toString());
211
            if (!sharpLinkMatch) {
212
                return;
213
            }
214
            const target = container.querySelector(`#${sharpLinkMatch[1]}`) as HTMLElement;
215
            if (target) {
216
                const top = getOffset(target, this.container()).top;
217
                if (top < scope) {
218
                    sections.push({
219
                        top,
220
                        linkComponent
221
                    });
222
                }
223
            }
224
        });
225

226
        this.visible = !!sections.length;
227
        if (!this.visible) {
228
            this.clearActive();
229
            this.cdr.detectChanges();
230
        } else {
231
            const maxSection = sections.reduce((prev, curr) => (curr.top > prev.top ? curr : prev));
232
            this.handleActive(maxSection.linkComponent);
233
        }
234
        this.setVisible();
235
    }
236

237
    private clearActive(): void {
238
        this.links.forEach(i => {
239
            i.unsetActive();
240
        });
241
    }
242

243
    private handleActive(linkComponent: ThyAnchorLink): void {
244
        this.clearActive();
245
        linkComponent.setActive();
246
        const linkNode = linkComponent.getLinkTitleElement();
247
        const horizontalAnchor = this.thyDirection() === 'horizontal';
248

249
        const ink = this.ink();
250
        ink.nativeElement.style.top = horizontalAnchor ? '' : `${linkNode.offsetTop}px`;
251
        ink.nativeElement.style.height = horizontalAnchor ? '' : `${linkNode.clientHeight}px`;
252
        ink.nativeElement.style.left = horizontalAnchor ? `${linkNode.offsetLeft}px` : '';
253
        ink.nativeElement.style.width = horizontalAnchor ? `${linkNode.clientWidth}px` : '';
254
        this.visible = true;
255
        this.setVisible();
256
        this.thyScroll.emit(linkComponent);
257
    }
258

259
    private setVisible(): void {
260
        const visible = this.visible;
261
        const visibleClassname = 'visible';
262
        const ink = this.ink();
263
        if (ink) {
264
            if (visible) {
265
                this.renderer.addClass(ink.nativeElement, visibleClassname);
266
            } else {
267
                this.renderer.removeClass(ink.nativeElement, visibleClassname);
268
            }
269
        }
270
    }
271

272
    handleScrollTo(linkComponent: ThyAnchorLink): void {
273
        const container: HTMLElement =
274
            this.container() instanceof HTMLElement ? (this.container() as HTMLElement) : (this.document as unknown as HTMLElement);
275
        const linkElement: HTMLElement = container.querySelector(linkComponent.thyHref());
276
        if (!linkElement) {
277
            return;
278
        }
279

280
        this.animating = true;
281
        const containerScrollTop = this.scrollService.getScroll(this.container());
282
        const elementOffsetTop = getOffset(linkElement, this.container()).top;
283
        const targetScrollTop = containerScrollTop + elementOffsetTop - (this.thyOffsetTop() || 0);
284
        this.scrollService.scrollTo(this.container(), targetScrollTop, undefined, () => {
285
            this.animating = false;
286
        });
287
        this.handleActive(linkComponent);
288
        this.thyClick.emit(linkComponent);
289
    }
290
}
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