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

atinc / ngx-tethys / 182a3d9b-8701-46d1-a8ee-c5fd8187d7b7

26 Dec 2023 06:18AM UTC coverage: 90.538% (-0.005%) from 90.543%
182a3d9b-8701-46d1-a8ee-c5fd8187d7b7

Pull #2991

circleci

smile1016
feat(property): add background when trigger is click #INFR-11089
Pull Request #2991: feat(property): add background when trigger is click merge master #INFR-11089

5403 of 6626 branches covered (0.0%)

Branch coverage included in aggregate %.

13486 of 14237 relevant lines covered (94.73%)

978.71 hits per line

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

87.9
/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
} from '@angular/core';
10✔
20
import { Subject, fromEvent } from 'rxjs';
10✔
21
import { takeUntil, throttleTime } from 'rxjs/operators';
10✔
22

10✔
23
import { DOCUMENT, NgClass, NgIf, NgStyle, NgTemplateOutlet } from '@angular/common';
10✔
24
import { ThyAffixComponent } from 'ngx-tethys/affix';
10✔
25
import { InputBoolean, InputNumber, ThyScrollService } from 'ngx-tethys/core';
10✔
26
import { getOffset } from 'ngx-tethys/util';
10✔
27
import { ThyAnchorLinkComponent } from './anchor-link.component';
10✔
28

10✔
29
interface Section {
10✔
30
    linkComponent: ThyAnchorLinkComponent;
10✔
31
    top: number;
10✔
32
}
10✔
33

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

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

11✔
70
    /**
71
     * 固定模式
×
72
     */
73
    @Input() @InputBoolean() thyAffix = true;
74

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

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

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

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

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

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

111
    visible = false;
112

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

115
    container?: HTMLElement | Window;
116

117
    private links: ThyAnchorLinkComponent[] = [];
6✔
118

6✔
119
    private animating = false;
6✔
120

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

6✔
123
    private handleScrollTimeoutID = -1;
6✔
124

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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