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

atinc / ngx-tethys / 3175bd09-7257-4520-af06-32481b06712b

10 Feb 2025 06:18AM UTC coverage: 90.278% (-0.002%) from 90.28%
3175bd09-7257-4520-af06-32481b06712b

Pull #3293

circleci

minlovehua
fix: ci error
Pull Request #3293: fix(nav): need to monitor the resize changes of the newly added nav item #TINFR-1492

5570 of 6825 branches covered (81.61%)

Branch coverage included in aggregate %.

16 of 16 new or added lines in 1 file covered. (100.0%)

7 existing lines in 2 files now uncovered.

13281 of 14056 relevant lines covered (94.49%)

991.61 hits per line

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

88.16
/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
    EventEmitter,
9
    Input,
10
    NgZone,
1✔
11
    OnChanges,
12
    OnDestroy,
13
    Output,
14
    Renderer2,
15
    SimpleChanges,
1✔
16
    ViewChild,
17
    ViewEncapsulation,
11✔
18
    numberAttribute,
11✔
19
    inject
11✔
20
} from '@angular/core';
11✔
21
import { Subject, fromEvent } from 'rxjs';
11✔
22
import { takeUntil, throttleTime } from 'rxjs/operators';
11✔
23

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

11✔
30
interface Section {
11✔
31
    linkComponent: ThyAnchorLink;
11✔
32
    top: number;
11✔
33
}
11✔
34

11✔
35
const sharpMatcherRegx = /#([^#]+)$/;
36

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

80
    @ViewChild('ink') private ink!: ElementRef;
4✔
81

4✔
82
    /**
4!
83
     * 固定模式
4✔
84
     */
12✔
85
    @Input({ transform: coerceBooleanProperty }) thyAffix = true;
12!
86

×
87
    /**
88
     * 锚点区域边界,单位:px
12✔
89
     */
12✔
90
    @Input({ transform: numberAttribute })
10✔
91
    thyBounds = 5;
10✔
92

4✔
93
    /**
94
     * 缓冲的偏移量阈值
95
     */
96
    @Input({ transform: numberAttribute })
97
    thyOffsetTop?: number = undefined;
98

99
    /**
4✔
100
     * 指定滚动的容器
4!
101
     * @type string | HTMLElement
×
102
     */
×
103
    @Input() thyContainer?: string | HTMLElement;
104

105
    /**
4!
106
     * 设置导航方向
4✔
107
     * @type 'vertical' | 'horizontal'
108
     */
4✔
109
    @Input() thyDirection: 'vertical' | 'horizontal' = 'vertical';
110

111
    /**
6✔
112
     * 点击项触发
22✔
113
     */
114
    @Output() readonly thyClick = new EventEmitter<ThyAnchorLink>();
115

116
    /**
6✔
117
     * 滚动到某锚点时触发
6✔
118
     */
6✔
119
    @Output() readonly thyScroll = new EventEmitter<ThyAnchorLink>();
6✔
120

6✔
121
    visible = false;
6✔
122

6✔
123
    wrapperStyle = { 'max-height': '100vh' };
6✔
124

6✔
125
    container?: HTMLElement | Window;
6✔
126

6✔
127
    private links: ThyAnchorLink[] = [];
128

129
    private animating = false;
10✔
130

10✔
131
    private destroy$ = new Subject<void>();
10!
132

10!
133
    private handleScrollTimeoutID: any = -1;
10✔
134

135
    registerLink(link: ThyAnchorLink): void {
136
        this.links.push(link);
×
137
    }
138

139
    unregisterLink(link: ThyAnchorLink): void {
140
        this.links.splice(this.links.indexOf(link), 1);
141
    }
3!
142

3✔
143
    private getContainer(): HTMLElement | Window {
3✔
144
        return this.container || window;
1✔
145
    }
146

2✔
147
    ngAfterViewInit(): void {
2✔
148
        this.warningPrompt();
2✔
149
        this.registerScrollEvent();
2!
150
    }
2✔
151

2✔
152
    ngOnDestroy(): void {
153
        clearTimeout(this.handleScrollTimeoutID);
2✔
154
        this.destroy$.next();
2✔
155
        this.destroy$.complete();
156
    }
157

11✔
158
    private warningPrompt() {
11!
159
        if (this.thyDirection === 'horizontal') {
11✔
160
            const hasChildren = this.links.some(link =>
161
                Array.from(link?.elementRef?.nativeElement?.childNodes)?.some((item: HTMLElement) => item?.nodeName === 'THY-ANCHOR-LINK')
162
            );
163
            if (hasChildren) {
11✔
164
                console.warn("Anchor link nesting is not supported when 'Anchor' direction is horizontal.");
1✔
165
            }
1!
166
        }
1✔
167
    }
168

169
    private registerScrollEvent(): void {
1✔
170
        if (!this.platform.isBrowser) {
171
            return;
172
        }
173
        this.destroy$.next();
174
        this.zone.runOutsideAngular(() => {
175
            fromEvent(this.getContainer(), 'scroll', { passive: true })
176
                .pipe(throttleTime(50), takeUntil(this.destroy$))
177
                .subscribe(() => this.handleScroll());
178
        });
179
        // Browser would maintain the scrolling position when refreshing.
180
        // So we have to delay calculation in avoid of getting a incorrect result.
1✔
181
        this.handleScrollTimeoutID = setTimeout(() => this.handleScroll());
182
    }
183

184
    handleScroll(): void {
185
        if (typeof document === 'undefined' || this.animating) {
186
            return;
187
        }
188
        const container: HTMLElement = this.container instanceof HTMLElement ? this.container : (this.document as unknown as HTMLElement);
189

190
        const sections: Section[] = [];
191
        const scope = (this.thyOffsetTop || 0) + this.thyBounds;
192
        this.links.forEach(linkComponent => {
193
            const sharpLinkMatch = sharpMatcherRegx.exec(linkComponent.thyHref.toString());
194
            if (!sharpLinkMatch) {
195
                return;
196
            }
197
            const target = container.querySelector(`#${sharpLinkMatch[1]}`) as HTMLElement;
198
            if (target) {
199
                const top = getOffset(target, this.getContainer()).top;
200
                if (top < scope) {
201
                    sections.push({
202
                        top,
203
                        linkComponent
204
                    });
205
                }
206
            }
207
        });
208

209
        this.visible = !!sections.length;
210
        if (!this.visible) {
211
            this.clearActive();
212
            this.cdr.detectChanges();
213
        } else {
214
            const maxSection = sections.reduce((prev, curr) => (curr.top > prev.top ? curr : prev));
215
            this.handleActive(maxSection.linkComponent);
216
        }
217
        this.setVisible();
218
    }
219

220
    private clearActive(): void {
221
        this.links.forEach(i => {
222
            i.unsetActive();
223
        });
224
    }
225

226
    private handleActive(linkComponent: ThyAnchorLink): void {
227
        this.clearActive();
228
        linkComponent.setActive();
229
        const linkNode = linkComponent.getLinkTitleElement();
230
        const horizontalAnchor = this.thyDirection === 'horizontal';
231

232
        this.ink.nativeElement.style.top = horizontalAnchor ? '' : `${linkNode.offsetTop}px`;
233
        this.ink.nativeElement.style.height = horizontalAnchor ? '' : `${linkNode.clientHeight}px`;
234
        this.ink.nativeElement.style.left = horizontalAnchor ? `${linkNode.offsetLeft}px` : '';
235
        this.ink.nativeElement.style.width = horizontalAnchor ? `${linkNode.clientWidth}px` : '';
236
        this.visible = true;
237
        this.setVisible();
238
        this.thyScroll.emit(linkComponent);
239
    }
240

241
    private setVisible(): void {
242
        const visible = this.visible;
243
        const visibleClassname = 'visible';
244
        if (this.ink) {
245
            if (visible) {
246
                this.renderer.addClass(this.ink.nativeElement, visibleClassname);
247
            } else {
248
                this.renderer.removeClass(this.ink.nativeElement, visibleClassname);
249
            }
250
        }
251
    }
252

253
    handleScrollTo(linkComponent: ThyAnchorLink): void {
254
        const container: HTMLElement = this.container instanceof HTMLElement ? this.container : (this.document as unknown as HTMLElement);
255
        const linkElement: HTMLElement = container.querySelector(linkComponent.thyHref);
256
        if (!linkElement) {
257
            return;
258
        }
259

260
        this.animating = true;
261
        const containerScrollTop = this.scrollService.getScroll(this.getContainer());
262
        const elementOffsetTop = getOffset(linkElement, this.getContainer()).top;
263
        const targetScrollTop = containerScrollTop + elementOffsetTop - (this.thyOffsetTop || 0);
264
        this.scrollService.scrollTo(this.getContainer(), targetScrollTop, undefined, () => {
265
            this.animating = false;
266
        });
267
        this.handleActive(linkComponent);
268
        this.thyClick.emit(linkComponent);
269
    }
270

271
    ngOnChanges(changes: SimpleChanges): void {
272
        const { thyOffsetTop, thyContainer } = changes;
273
        if (thyOffsetTop) {
274
            this.wrapperStyle = {
275
                'max-height': `calc(100vh - ${this.thyOffsetTop}px)`
276
            };
277
        }
278
        if (thyContainer && this.thyContainer) {
279
            const container = this.thyContainer;
280
            this.container = typeof container === 'string' ? (this.document.querySelector(container) as HTMLElement) : container;
281
            this.registerScrollEvent();
282
        }
283
    }
284
}
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