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

atinc / ngx-tethys / 84cc88fc-0911-40c7-b19a-63c7c0e844db

21 Aug 2024 03:17AM UTC coverage: 90.462% (-0.005%) from 90.467%
84cc88fc-0911-40c7-b19a-63c7c0e844db

Pull #3163

circleci

wangyuan-ky
build: fix comment  #TINFR-396
Pull Request #3163: build: solve scss warnings #TINFR-396 @wumeimin @xinglu

5497 of 6721 branches covered (81.79%)

Branch coverage included in aggregate %.

13245 of 13997 relevant lines covered (94.63%)

996.33 hits per line

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

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

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

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

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

37
/**
38
 * 锚点组件
44✔
39
 * @name thy-anchor
40
 */
41
@Component({
44✔
42
    selector: 'thy-anchor',
43
    exportAs: 'thyAnchor',
44
    preserveWhitespaces: false,
27✔
45
    template: `
46
        <thy-affix *ngIf="thyAffix; else content" [thyOffsetTop]="thyOffsetTop" [thyContainer]="container">
47
            <ng-template [ngTemplateOutlet]="content"></ng-template>
10✔
48
        </thy-affix>
10✔
49
        <ng-template #content>
50
            <div
51
                class="thy-anchor-wrapper"
10✔
52
                [ngClass]="{ 'thy-anchor-wrapper-horizontal': thyDirection === 'horizontal' }"
10✔
53
                [ngStyle]="wrapperStyle">
10✔
54
                <div class="thy-anchor">
55
                    <div class="thy-anchor-ink">
56
                        <div class="thy-anchor-ink-full" #ink></div>
10✔
57
                    </div>
4✔
58
                    <ng-content></ng-content>
4!
59
                </div>
4✔
60
            </div>
61
        </ng-template>
62
    `,
63
    encapsulation: ViewEncapsulation.None,
64
    changeDetection: ChangeDetectionStrategy.OnPush,
11!
65
    standalone: true,
×
66
    imports: [NgIf, ThyAffix, NgTemplateOutlet, NgStyle, NgClass]
67
})
11✔
68
export class ThyAnchor implements OnDestroy, AfterViewInit, OnChanges {
11✔
69
    @ViewChild('ink') private ink!: ElementRef;
11✔
70

71
    /**
×
72
     * 固定模式
73
     */
74
    @Input({ transform: coerceBooleanProperty }) thyAffix = true;
75

11✔
76
    /**
77
     * 锚点区域边界,单位:px
78
     */
5✔
79
    @Input({ transform: numberAttribute })
1✔
80
    thyBounds = 5;
81

4✔
82
    /**
4✔
83
     * 缓冲的偏移量阈值
4!
84
     */
4✔
85
    @Input({ transform: numberAttribute })
12✔
86
    thyOffsetTop?: number = undefined;
12!
87

×
88
    /**
89
     * 指定滚动的容器
12✔
90
     * @type string | HTMLElement
12✔
91
     */
10✔
92
    @Input() thyContainer?: string | HTMLElement;
10✔
93

4✔
94
    /**
95
     * 设置导航方向
96
     * @type 'vertical' | 'horizontal'
97
     */
98
    @Input() thyDirection: 'vertical' | 'horizontal' = 'vertical';
99

100
    /**
4✔
101
     * 点击项触发
4!
102
     */
×
103
    @Output() readonly thyClick = new EventEmitter<ThyAnchorLink>();
×
104

105
    /**
106
     * 滚动到某锚点时触发
4!
107
     */
4✔
108
    @Output() readonly thyScroll = new EventEmitter<ThyAnchorLink>();
109

4✔
110
    visible = false;
111

112
    wrapperStyle = { 'max-height': '100vh' };
6✔
113

22✔
114
    container?: HTMLElement | Window;
115

116
    private links: ThyAnchorLink[] = [];
117

6✔
118
    private animating = false;
6✔
119

6✔
120
    private destroy$ = new Subject<void>();
6✔
121

6✔
122
    private handleScrollTimeoutID: any = -1;
6✔
123

6✔
124
    constructor(
6✔
125
        @Inject(DOCUMENT) private document: any,
6✔
126
        private cdr: ChangeDetectorRef,
6✔
127
        private platform: Platform,
6✔
128
        private zone: NgZone,
129
        private renderer: Renderer2,
130
        private scrollService: ThyScrollService,
10✔
131
        private elementRef: ElementRef
10✔
132
    ) {}
10!
133

10!
134
    registerLink(link: ThyAnchorLink): void {
10✔
135
        this.links.push(link);
136
    }
137

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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