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

atinc / ngx-tethys / 19f428d7-bff5-493d-baab-aab74f4ebf3d

29 Aug 2023 03:14PM UTC coverage: 90.118% (-0.003%) from 90.121%
19f428d7-bff5-493d-baab-aab74f4ebf3d

Pull #2815

circleci

minlovehua
feat(dialog): add cdkScrollable for scrollable element dialog-body #INFR-8826
Pull Request #2815: feat(dialog): add cdkScrollable for scrollable element dialog-body #INFR-8826

5128 of 6349 branches covered (0.0%)

Branch coverage included in aggregate %.

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

12964 of 13727 relevant lines covered (94.44%)

974.76 hits per line

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

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

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

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

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

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

67
    /**
3!
68
     * 固定模式
×
69
     */
70
    @Input() @InputBoolean() thyAffix = true;
3✔
71

3✔
72
    /**
3!
73
     * 锚点区域边界,单位:px
3✔
74
     */
8✔
75
    @Input()
8!
76
    @InputNumber()
×
77
    thyBounds = 5;
78

8✔
79
    /**
8✔
80
     * 缓冲的偏移量阈值
7✔
81
     */
7✔
82
    @Input()
3✔
83
    @InputNumber()
84
    thyOffsetTop?: number = undefined;
85

86
    /**
87
     * 指定滚动的容器
88
     * @type string | HTMLElement
89
     */
3✔
90
    @Input() thyContainer?: string | HTMLElement;
3!
91

×
92
    /**
×
93
     * 点击项触发
94
     */
95
    @Output() readonly thyClick = new EventEmitter<ThyAnchorLinkComponent>();
3!
96

3✔
97
    /**
98
     * 滚动到某锚点时触发
3✔
99
     */
100
    @Output() readonly thyScroll = new EventEmitter<ThyAnchorLinkComponent>();
101

4✔
102
    visible = false;
14✔
103

104
    wrapperStyle = { 'max-height': '100vh' };
105

106
    container?: HTMLElement | Window;
4✔
107

4✔
108
    private links: ThyAnchorLinkComponent[] = [];
4✔
109

4✔
110
    private animating = false;
4✔
111

4✔
112
    private destroy$ = new Subject<void>();
4✔
113

4✔
114
    private handleScrollTimeoutID = -1;
115

116
    constructor(
7✔
117
        @Inject(DOCUMENT) private document: any,
7✔
118
        private cdr: ChangeDetectorRef,
7!
119
        private platform: Platform,
7!
120
        private zone: NgZone,
7✔
121
        private renderer: Renderer2,
122
        private scrollService: ThyScrollService
123
    ) {}
×
124

125
    registerLink(link: ThyAnchorLinkComponent): void {
126
        this.links.push(link);
127
    }
128

2!
129
    unregisterLink(link: ThyAnchorLinkComponent): void {
2✔
130
        this.links.splice(this.links.indexOf(link), 1);
2✔
131
    }
1✔
132

133
    private getContainer(): HTMLElement | Window {
1✔
134
        return this.container || window;
1✔
135
    }
1✔
136

1!
137
    ngAfterViewInit(): void {
1✔
138
        this.registerScrollEvent();
1✔
139
    }
140

1✔
141
    ngOnDestroy(): void {
1✔
142
        clearTimeout(this.handleScrollTimeoutID);
143
        this.destroy$.next();
144
        this.destroy$.complete();
6✔
145
    }
6!
146

6✔
147
    private registerScrollEvent(): void {
148
        if (!this.platform.isBrowser) {
149
            return;
150
        }
6✔
151
        this.destroy$.next();
1✔
152
        this.zone.runOutsideAngular(() => {
1!
153
            fromEvent(this.getContainer(), 'scroll', { passive: true })
1✔
154
                .pipe(throttleTime(50), takeUntil(this.destroy$))
155
                .subscribe(() => this.handleScroll());
156
        });
1✔
157
        // Browser would maintain the scrolling position when refreshing.
158
        // So we have to delay calculation in avoid of getting a incorrect result.
159
        this.handleScrollTimeoutID = setTimeout(() => this.handleScroll());
160
    }
161

162
    handleScroll(): void {
163
        if (typeof document === 'undefined' || this.animating) {
164
            return;
1✔
165
        }
166
        const container: HTMLElement = this.container instanceof HTMLElement ? this.container : this.document;
167

168
        const sections: Section[] = [];
169
        const scope = (this.thyOffsetTop || 0) + this.thyBounds;
170
        this.links.forEach(linkComponent => {
171
            const sharpLinkMatch = sharpMatcherRegx.exec(linkComponent.thyHref.toString());
172
            if (!sharpLinkMatch) {
173
                return;
174
            }
1✔
175
            const target = container.querySelector(`#${sharpLinkMatch[1]}`) as HTMLElement;
176
            if (target) {
177
                const top = getOffset(target, this.getContainer()).top;
178
                if (top < scope) {
1✔
179
                    sections.push({
180
                        top,
181
                        linkComponent
182
                    });
1✔
183
                }
184
            }
185
        });
186

1✔
187
        this.visible = !!sections.length;
188
        if (!this.visible) {
189
            this.clearActive();
190
            this.cdr.detectChanges();
191
        } else {
192
            const maxSection = sections.reduce((prev, curr) => (curr.top > prev.top ? curr : prev));
193
            this.handleActive(maxSection.linkComponent);
194
        }
195
        this.setVisible();
196
    }
197

198
    private clearActive(): void {
199
        this.links.forEach(i => {
200
            i.unsetActive();
201
        });
202
    }
203

204
    private handleActive(linkComponent: ThyAnchorLinkComponent): void {
205
        this.clearActive();
206
        linkComponent.setActive();
207
        const linkNode = linkComponent.getLinkTitleElement();
208

209
        this.ink.nativeElement.style.top = `${linkNode.offsetTop}px`;
210
        this.ink.nativeElement.style.height = `${linkNode.clientHeight}px`;
211
        this.visible = true;
212
        this.setVisible();
213
        this.thyScroll.emit(linkComponent);
214
    }
215

216
    private setVisible(): void {
217
        const visible = this.visible;
218
        const visibleClassname = 'visible';
219
        if (this.ink) {
220
            if (visible) {
221
                this.renderer.addClass(this.ink.nativeElement, visibleClassname);
222
            } else {
223
                this.renderer.removeClass(this.ink.nativeElement, visibleClassname);
224
            }
225
        }
226
    }
227

228
    handleScrollTo(linkComponent: ThyAnchorLinkComponent): void {
229
        const container: HTMLElement = this.container instanceof HTMLElement ? this.container : this.document;
230
        const linkElement: HTMLElement = container.querySelector(linkComponent.thyHref);
231
        if (!linkElement) {
232
            return;
233
        }
234

235
        this.animating = true;
236
        const containerScrollTop = this.scrollService.getScroll(this.getContainer());
237
        const elementOffsetTop = getOffset(linkElement, this.getContainer()).top;
238
        const targetScrollTop = containerScrollTop + elementOffsetTop - (this.thyOffsetTop || 0);
239
        this.scrollService.scrollTo(this.getContainer(), targetScrollTop, undefined, () => {
240
            this.animating = false;
241
        });
242
        this.handleActive(linkComponent);
243
        this.thyClick.emit(linkComponent);
244
    }
245

246
    ngOnChanges(changes: SimpleChanges): void {
247
        const { thyOffsetTop, thyContainer } = changes;
248
        if (thyOffsetTop) {
249
            this.wrapperStyle = {
250
                'max-height': `calc(100vh - ${this.thyOffsetTop}px)`
251
            };
252
        }
253
        if (thyContainer && this.thyContainer) {
254
            const container = this.thyContainer;
255
            this.container = typeof container === 'string' ? this.document.querySelector(container) : container;
256
            this.registerScrollEvent();
257
        }
258
    }
259
}
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