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

atinc / ngx-tethys / d9ae709b-3c27-4b69-b125-b8b80b54f90b

pending completion
d9ae709b-3c27-4b69-b125-b8b80b54f90b

Pull #2757

circleci

mengshuicmq
fix: fix code review
Pull Request #2757: feat(color-picker): color-picker support disabled (#INFR-8645)

98 of 6315 branches covered (1.55%)

Branch coverage included in aggregate %.

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

2392 of 13661 relevant lines covered (17.51%)

83.12 hits per line

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

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

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

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

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

36
/**
×
37
 * 锚点组件
38
 * @name thy-anchor
39
 */
×
40
@Component({
41
    selector: 'thy-anchor',
42
    exportAs: 'thyAnchor',
×
43
    preserveWhitespaces: false,
44
    template: `
45
        <thy-affix *ngIf="thyAffix; else content" [thyOffsetTop]="thyOffsetTop" [thyContainer]="container">
×
46
            <ng-template [ngTemplateOutlet]="content"></ng-template>
47
        </thy-affix>
48
        <ng-template #content>
×
49
            <div class="thy-anchor-wrapper" [ngStyle]="wrapperStyle">
×
50
                <div class="thy-anchor">
×
51
                    <div class="thy-anchor-ink">
52
                        <div class="thy-anchor-ink-full" #ink></div>
53
                    </div>
×
54
                    <ng-content></ng-content>
×
55
                </div>
56
            </div>
×
57
        </ng-template>
×
58
    `,
×
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 {
×
65
    @ViewChild('ink') private ink!: ElementRef;
66

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

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

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

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

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

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

×
102
    visible = false;
×
103

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

106
    container?: HTMLElement | Window;
×
107

×
108
    private links: ThyAnchorLinkComponent[] = [];
×
109

×
110
    private animating = false;
×
111

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

×
114
    private handleScrollTimeoutID = -1;
115

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

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

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

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

×
137
    ngAfterViewInit(): void {
×
138
        this.registerScrollEvent();
×
139
    }
140

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

×
147
    private registerScrollEvent(): void {
148
        if (!this.platform.isBrowser) {
149
            return;
150
        }
×
151
        this.destroy$.next();
×
152
        this.zone.runOutsideAngular(() => {
×
153
            fromEvent(this.getContainer(), 'scroll', { passive: true })
×
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