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

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

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

3.64
/src/affix/affix.component.ts
1
import { ThyScrollService } from 'ngx-tethys/core';
2
import { dom, shallowEqual, SimpleRect } from 'ngx-tethys/util';
3
import { fromEvent, merge, ReplaySubject, Subject, Subscription } from 'rxjs';
4
import { auditTime, map, takeUntil } from 'rxjs/operators';
5

6
import { Platform } from '@angular/cdk/platform';
7
import { DOCUMENT } from '@angular/common';
8
import {
9
    AfterViewInit,
10
    ChangeDetectionStrategy,
11
    Component,
1✔
12
    computed,
1✔
13
    effect,
14
    ElementRef,
15
    inject,
16
    input,
17
    NgZone,
18
    numberAttribute,
1✔
19
    OnDestroy,
UNCOV
20
    output,
×
UNCOV
21
    Renderer2,
×
UNCOV
22
    Signal,
×
UNCOV
23
    viewChild,
×
UNCOV
24
    ViewEncapsulation
×
UNCOV
25
} from '@angular/core';
×
UNCOV
26

×
UNCOV
27
import { AffixRespondEvents } from './respond-events';
×
UNCOV
28

×
UNCOV
29
const THY_AFFIX_CLS_PREFIX = 'thy-affix';
×
UNCOV
30
const THY_AFFIX_DEFAULT_SCROLL_TIME = 20;
×
UNCOV
31

×
UNCOV
32
/**
×
UNCOV
33
 * 固钉组件
×
UNCOV
34
 * @name thy-affix
×
35
 * @order 10
UNCOV
36
 */
×
UNCOV
37
@Component({
×
38
    selector: 'thy-affix',
UNCOV
39
    exportAs: 'thyAffix',
×
UNCOV
40
    template: `
×
UNCOV
41
        <div #fixedElement>
×
UNCOV
42
            <ng-content></ng-content>
×
UNCOV
43
        </div>
×
44
    `,
45
    changeDetection: ChangeDetectionStrategy.OnPush,
46
    encapsulation: ViewEncapsulation.None
47
})
UNCOV
48
export class ThyAffix implements AfterViewInit, OnDestroy {
×
49
    private scrollService = inject(ThyScrollService);
50
    private ngZone = inject(NgZone);
UNCOV
51
    private platform = inject(Platform);
×
52
    private renderer = inject(Renderer2);
53

UNCOV
54
    private readonly fixedElement = viewChild.required<ElementRef<HTMLDivElement>>('fixedElement');
×
UNCOV
55

×
UNCOV
56
    /**
×
57
     * 设置 thy-affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数
UNCOV
58
     * @default window
×
59
     * @type string | Element | Window
UNCOV
60
     */
×
61
    readonly thyContainer = input<string | Element | Window>();
62

UNCOV
63
    /**
×
UNCOV
64
     * 距离窗口顶部缓冲的偏移量阈值
×
UNCOV
65
     */
×
UNCOV
66
    readonly thyOffsetTop = input<number, unknown>(0, { transform: numberAttribute });
×
67

68
    /**
UNCOV
69
     * 距离窗口底部缓冲的偏移量阈值
×
UNCOV
70
     */
×
UNCOV
71
    readonly thyOffsetBottom = input<number, unknown>(0, { transform: numberAttribute });
×
UNCOV
72

×
UNCOV
73
    /**
×
UNCOV
74
     * 固定状态改变时触发的回调函数
×
UNCOV
75
     */
×
UNCOV
76
    readonly thyChange = output<boolean>();
×
77

78
    private readonly placeholderNode: HTMLElement;
79

80
    private affixStyle?: any;
81
    private placeholderStyle?: any;
82
    private positionChangeSubscription: Subscription = Subscription.EMPTY;
83
    private offsetChanged$ = new ReplaySubject(1);
UNCOV
84
    private destroy$ = new Subject<void>();
×
UNCOV
85
    private timeout?: any;
×
UNCOV
86
    private document: any;
×
87

×
88
    private readonly container: Signal<Element | Window> = computed(() => {
UNCOV
89
        const el = this.thyContainer();
×
UNCOV
90
        return (typeof el === 'string' ? this.document.querySelector(el) : el) || window;
×
91
    });
UNCOV
92

×
UNCOV
93
    constructor() {
×
UNCOV
94
        const el = inject(ElementRef);
×
UNCOV
95
        const document = inject(DOCUMENT);
×
UNCOV
96

×
UNCOV
97
        // The wrapper would stay at the original position as a placeholder.
×
98
        this.placeholderNode = el.nativeElement;
99
        this.document = document;
100
        effect(() => {
×
101
            if (this.thyOffsetBottom() || this.thyOffsetTop()) {
UNCOV
102
                this.offsetChanged$.next(undefined);
×
UNCOV
103
            }
×
104
        });
105
    }
106

UNCOV
107
    ngAfterViewInit(): void {
×
UNCOV
108
        this.registerListeners();
×
UNCOV
109
    }
×
110

UNCOV
111
    ngOnDestroy(): void {
×
UNCOV
112
        this.removeListeners();
×
113
    }
114

115
    private registerListeners(): void {
×
116
        this.removeListeners();
×
117
        this.positionChangeSubscription = this.ngZone.runOutsideAngular(() => {
118
            return merge(
×
119
                ...Object.keys(AffixRespondEvents).map(evName => fromEvent(this.container(), evName)),
×
120
                this.offsetChanged$.pipe(
×
121
                    takeUntil(this.destroy$),
122
                    map(() => ({}))
123
                )
124
            )
×
125
                .pipe(auditTime(THY_AFFIX_DEFAULT_SCROLL_TIME))
126
                .subscribe(e => this.updatePosition(e as Event));
127
        });
128
        this.timeout = setTimeout(() => this.updatePosition({} as Event));
×
129
    }
130

UNCOV
131
    private removeListeners(): void {
×
132
        clearTimeout(this.timeout);
×
133
        this.positionChangeSubscription.unsubscribe();
UNCOV
134
        this.destroy$.next();
×
UNCOV
135
        this.destroy$.complete();
×
UNCOV
136
    }
×
UNCOV
137

×
UNCOV
138
    getOffset(element: Element, target: Element | Window | undefined): SimpleRect {
×
UNCOV
139
        const elemRect = element.getBoundingClientRect();
×
140
        const containerRect = dom.getContainerRect(target);
141

142
        const scrollTop = this.scrollService.getScroll(target, true);
UNCOV
143
        const scrollLeft = this.scrollService.getScroll(target, false);
×
144

145
        const docElem = this.document.body;
146
        const clientTop = docElem.clientTop || 0;
147
        const clientLeft = docElem.clientLeft || 0;
UNCOV
148

×
UNCOV
149
        return {
×
150
            top: elemRect.top - containerRect.top + scrollTop - clientTop,
×
151
            left: elemRect.left - containerRect.left + scrollLeft - clientLeft,
×
152
            width: elemRect.width,
153
            height: elemRect.height
UNCOV
154
        };
×
UNCOV
155
    }
×
156

UNCOV
157
    private setAffixStyle(e: Event, affixStyle?: any): void {
×
UNCOV
158
        const originalAffixStyle = this.affixStyle;
×
UNCOV
159
        const isWindow = this.container() === window;
×
UNCOV
160
        if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) {
×
UNCOV
161
            return;
×
UNCOV
162
        }
×
163
        if (shallowEqual(originalAffixStyle, affixStyle)) {
164
            return;
165
        }
166

167
        const fixed = !!affixStyle;
UNCOV
168
        const wrapElement = this.fixedElement().nativeElement;
×
169
        this.renderer.setStyle(wrapElement, 'cssText', dom.getStyleAsText(affixStyle));
170
        this.affixStyle = affixStyle;
171
        if (fixed) {
172
            wrapElement.classList.add(THY_AFFIX_CLS_PREFIX);
UNCOV
173
        } else {
×
174
            wrapElement.classList.remove(THY_AFFIX_CLS_PREFIX);
175
        }
×
176

×
177
        if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) {
×
178
            this.thyChange.emit(fixed);
179
        }
180
    }
181

182
    private setPlaceholderStyle(placeholderStyle?: any): void {
183
        const originalPlaceholderStyle = this.placeholderStyle;
×
184
        if (shallowEqual(placeholderStyle, originalPlaceholderStyle)) {
185
            return;
186
        }
187
        this.renderer.setStyle(this.placeholderNode, 'cssText', dom.getStyleAsText(placeholderStyle));
188
        this.placeholderStyle = placeholderStyle;
UNCOV
189
    }
×
190

191
    private syncPlaceholderStyle(e: Event): void {
192
        if (!this.affixStyle) {
193
            return;
×
194
        }
195
        this.renderer.setStyle(this.placeholderNode, 'cssText', '');
196
        this.placeholderStyle = undefined;
197
        const styleObj = {
198
            width: this.placeholderNode.offsetWidth,
UNCOV
199
            height: this.fixedElement().nativeElement.offsetHeight
×
200
        };
UNCOV
201
        this.setAffixStyle(e, {
×
202
            ...this.affixStyle,
UNCOV
203
            ...styleObj
×
204
        });
×
205
        this.setPlaceholderStyle(styleObj);
206
    }
207

1✔
208
    updatePosition(e: Event): void {
1✔
209
        if (!this.platform.isBrowser) {
210
            return;
211
        }
212

213
        const containerNode = this.container();
214
        let offsetTop = this.thyOffsetTop();
215
        const scrollTop = this.scrollService.getScroll(containerNode, true);
216
        const elementOffset = this.getOffset(this.placeholderNode, containerNode);
1✔
217
        const fixedNode = this.fixedElement().nativeElement;
218
        const elemSize = {
219
            width: fixedNode.offsetWidth,
220
            height: fixedNode.offsetHeight
221
        };
222
        const offsetMode = {
223
            top: false,
224
            bottom: false
225
        };
226
        // Default to `offsetTop=0`.
227
        const thyOffsetBottom = this.thyOffsetBottom();
228
        if (typeof offsetTop !== 'number' && typeof thyOffsetBottom !== 'number') {
229
            offsetMode.top = true;
230
            offsetTop = 0;
231
        } else {
232
            offsetMode.top = typeof offsetTop === 'number';
233
            offsetMode.bottom = typeof thyOffsetBottom === 'number';
234
        }
235
        const containerRect = dom.getContainerRect(containerNode as Window);
236
        const targetInnerHeight = (containerNode as Window).innerHeight || (containerNode as HTMLElement).clientHeight;
237
        if (scrollTop >= elementOffset.top - (offsetTop as number) && offsetMode.top) {
238
            const width = elementOffset.width;
239
            const top = containerRect.top + (offsetTop as number);
240
            this.setAffixStyle(e, {
241
                position: 'fixed',
242
                top,
243
                left: containerRect.left + elementOffset.left,
244
                width
245
            });
246
            this.setPlaceholderStyle({
247
                width,
248
                height: elemSize.height
249
            });
250
        } else if (
251
            scrollTop <= elementOffset.top + elemSize.height + (thyOffsetBottom as number) - targetInnerHeight &&
252
            offsetMode.bottom
253
        ) {
254
            const targetBottomOffset = containerNode === window ? 0 : window.innerHeight - containerRect.bottom;
255
            const width = elementOffset.width;
256
            this.setAffixStyle(e, {
257
                position: 'fixed',
258
                bottom: targetBottomOffset + (thyOffsetBottom as number),
259
                left: containerRect.left + elementOffset.left,
260
                width
261
            });
262
            this.setPlaceholderStyle({
263
                width,
264
                height: elementOffset.height
265
            });
266
        } else {
267
            if (
268
                e.type === AffixRespondEvents.resize &&
269
                this.affixStyle &&
270
                this.affixStyle.position === 'fixed' &&
271
                this.placeholderNode.offsetWidth
272
            ) {
273
                this.setAffixStyle(e, {
274
                    ...this.affixStyle,
275
                    width: this.placeholderNode.offsetWidth
276
                });
277
            } else {
278
                this.setAffixStyle(e);
279
            }
280
            this.setPlaceholderStyle();
281
        }
282

283
        if (e.type === 'resize') {
284
            this.syncPlaceholderStyle(e);
285
        }
286
    }
287
}
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