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

atinc / ngx-tethys / f30ff593-df5e-4f5b-aa58-0de96ae910d0

30 Sep 2024 02:43AM UTC coverage: 90.431% (-0.005%) from 90.436%
f30ff593-df5e-4f5b-aa58-0de96ae910d0

push

circleci

why520crazy
docs: update examples of dialog

5511 of 6738 branches covered (81.79%)

Branch coverage included in aggregate %.

13257 of 14016 relevant lines covered (94.58%)

993.35 hits per line

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

88.24
/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,
11✔
18
    ViewEncapsulation,
11✔
19
    numberAttribute
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, NgIf, 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
        <thy-affix *ngIf="thyAffix; else content" [thyOffsetTop]="thyOffsetTop" [thyContainer]="container">
11✔
47
            <ng-template [ngTemplateOutlet]="content"></ng-template>
11✔
48
        </thy-affix>
49
        <ng-template #content>
50
            <div
11✔
51
                class="thy-anchor-wrapper"
11✔
52
                [ngClass]="{ 'thy-anchor-wrapper-horizontal': thyDirection === 'horizontal' }"
11✔
53
                [ngStyle]="wrapperStyle">
54
                <div class="thy-anchor">
55
                    <div class="thy-anchor-ink">
11✔
56
                        <div class="thy-anchor-ink-full" #ink></div>
25✔
57
                    </div>
5✔
58
                    <ng-content></ng-content>
1✔
59
                </div>
60
            </div>
61
        </ng-template>
62
    `,
63
    encapsulation: ViewEncapsulation.None,
12!
64
    changeDetection: ChangeDetectionStrategy.OnPush,
×
65
    standalone: true,
66
    imports: [NgIf, ThyAffix, NgTemplateOutlet, NgStyle, NgClass]
12✔
67
})
12✔
68
export class ThyAnchor implements OnDestroy, AfterViewInit, OnChanges {
12✔
69
    @ViewChild('ink') private ink!: ElementRef;
70

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

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

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

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

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

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

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

110
    visible = false;
111

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

114
    container?: HTMLElement | Window;
115

116
    private links: ThyAnchorLink[] = [];
6✔
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,
128
        private zone: NgZone,
129
        private renderer: Renderer2,
10✔
130
        private scrollService: ThyScrollService
10✔
131
    ) {}
10!
132

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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