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

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 hits per line

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

73.29
/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,
1✔
11
    Component,
1✔
12
    ElementRef,
13
    EventEmitter,
14
    Inject,
15
    Input,
16
    NgZone,
17
    numberAttribute,
1✔
18
    OnChanges,
19
    OnDestroy,
150✔
20
    Output,
150!
21
    Renderer2,
22
    SimpleChanges,
23
    ViewChild,
10✔
24
    ViewEncapsulation
10✔
25
} from '@angular/core';
10✔
26

10✔
27
import { AffixRespondEvents } from './respond-events';
10✔
28

10✔
29
const THY_AFFIX_CLS_PREFIX = 'thy-affix';
10✔
30
const THY_AFFIX_DEFAULT_SCROLL_TIME = 20;
10✔
31

32
/**
10✔
33
 * 固钉组件
10✔
34
 * @name thy-affix
35
 * @order 10
36
 */
10✔
37
@Component({
10!
38
    selector: 'thy-affix',
10✔
39
    exportAs: 'thyAffix',
40
    template: `
10!
41
        <div #fixedElement>
10✔
42
            <ng-content></ng-content>
43
        </div>
44
    `,
45
    changeDetection: ChangeDetectionStrategy.OnPush,
10✔
46
    encapsulation: ViewEncapsulation.None,
47
    standalone: true
48
})
10✔
49
export class ThyAffix implements AfterViewInit, OnChanges, OnDestroy {
50
    @ViewChild('fixedElement', { static: true }) private fixedElement!: ElementRef<HTMLDivElement>;
51

20✔
52
    /**
20✔
53
     * 设置 thy-affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数
140✔
54
     * @default window
55
     * @type string | Element | Window
1✔
56
     */
57
    @Input() thyContainer?: string | Element | Window;
20✔
58

59
    /**
60
     * 距离窗口顶部缓冲的偏移量阈值
30✔
61
     * @default 0
30✔
62
     */
30✔
63
    @Input({ transform: numberAttribute })
30✔
64
    thyOffsetTop?: null | number;
65

66
    /**
5✔
67
     * 距离窗口底部缓冲的偏移量阈值
5✔
68
     */
5✔
69
    @Input({ transform: numberAttribute })
5✔
70
    thyOffsetBottom?: null | number;
5✔
71

5✔
72
    /**
5✔
73
     * 固定状态改变时触发的回调函数
5✔
74
     */
75
    @Output() readonly thyChange = new EventEmitter<boolean>();
76

77
    private readonly placeholderNode: HTMLElement;
78

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

5✔
87
    private get container(): Element | Window {
2✔
88
        const el = this.thyContainer;
89
        return (typeof el === 'string' ? this.document.querySelector(el) : el) || window;
3✔
90
    }
3✔
91

3✔
92
    constructor(
3✔
93
        el: ElementRef,
3!
94
        @Inject(DOCUMENT) document: any,
3✔
95
        private scrollService: ThyScrollService,
96
        private ngZone: NgZone,
UNCOV
97
        private platform: Platform,
×
98
        private renderer: Renderer2
99
    ) {
3!
100
        // The wrapper would stay at the original position as a placeholder.
3✔
101
        this.placeholderNode = el.nativeElement;
102
        this.document = document;
103
    }
104

5✔
105
    ngOnChanges(changes: SimpleChanges): void {
5✔
106
        const { thyOffsetBottom, thyOffsetTop, thyContainer } = changes;
2✔
107

108
        if (thyOffsetBottom || thyOffsetTop) {
3✔
109
            this.offsetChanged$.next(undefined);
3✔
110
        }
111
        if (thyContainer) {
112
            this.registerListeners();
×
UNCOV
113
        }
×
114
    }
115

×
116
    ngAfterViewInit(): void {
×
UNCOV
117
        this.registerListeners();
×
118
    }
119

120
    ngOnDestroy(): void {
UNCOV
121
        this.removeListeners();
×
122
    }
123

124
    private registerListeners(): void {
UNCOV
125
        this.removeListeners();
×
126
        this.positionChangeSubscription = this.ngZone.runOutsideAngular(() => {
127
            return merge(
128
                ...Object.keys(AffixRespondEvents).map(evName => fromEvent(this.container, evName)),
5!
UNCOV
129
                this.offsetChanged$.pipe(
×
130
                    takeUntil(this.destroy$),
131
                    map(() => ({}))
5✔
132
                )
5✔
133
            )
5✔
134
                .pipe(auditTime(THY_AFFIX_DEFAULT_SCROLL_TIME))
5✔
135
                .subscribe(e => this.updatePosition(e as Event));
5✔
136
        });
5✔
137
        this.timeout = setTimeout(() => this.updatePosition({} as Event));
138
    }
139

140
    private removeListeners(): void {
5✔
141
        clearTimeout(this.timeout);
142
        this.positionChangeSubscription.unsubscribe();
143
        this.destroy$.next();
144
        this.destroy$.complete();
145
    }
5!
146

×
UNCOV
147
    getOffset(element: Element, target: Element | Window | undefined): SimpleRect {
×
148
        const elemRect = element.getBoundingClientRect();
149
        const containerRect = dom.getContainerRect(target);
150

5✔
151
        const scrollTop = this.scrollService.getScroll(target, true);
5✔
152
        const scrollLeft = this.scrollService.getScroll(target, false);
153

5✔
154
        const docElem = this.document.body;
5✔
155
        const clientTop = docElem.clientTop || 0;
5✔
156
        const clientLeft = docElem.clientLeft || 0;
3✔
157

3✔
158
        return {
3✔
159
            top: elemRect.top - containerRect.top + scrollTop - clientTop,
160
            left: elemRect.left - containerRect.left + scrollLeft - clientLeft,
161
            width: elemRect.width,
162
            height: elemRect.height
163
        };
164
    }
3✔
165

166
    private setAffixStyle(e: Event, affixStyle?: any): void {
167
        const originalAffixStyle = this.affixStyle;
168
        const isWindow = this.container === window;
169
        if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) {
2!
170
            return;
171
        }
×
172
        if (shallowEqual(originalAffixStyle, affixStyle)) {
×
UNCOV
173
            return;
×
174
        }
175

176
        const fixed = !!affixStyle;
177
        const wrapElement = this.fixedElement.nativeElement;
178
        this.renderer.setStyle(wrapElement, 'cssText', dom.getStyleAsText(affixStyle));
UNCOV
179
        this.affixStyle = affixStyle;
×
180
        if (fixed) {
181
            wrapElement.classList.add(THY_AFFIX_CLS_PREFIX);
182
        } else {
183
            wrapElement.classList.remove(THY_AFFIX_CLS_PREFIX);
184
        }
185

2!
186
        if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) {
187
            this.thyChange.emit(fixed);
188
        }
UNCOV
189
    }
×
190

191
    private setPlaceholderStyle(placeholderStyle?: any): void {
192
        const originalPlaceholderStyle = this.placeholderStyle;
193
        if (shallowEqual(placeholderStyle, originalPlaceholderStyle)) {
194
            return;
195
        }
2✔
196
        this.renderer.setStyle(this.placeholderNode, 'cssText', dom.getStyleAsText(placeholderStyle));
197
        this.placeholderStyle = placeholderStyle;
2✔
198
    }
199

5!
UNCOV
200
    private syncPlaceholderStyle(e: Event): void {
×
201
        if (!this.affixStyle) {
202
            return;
203
        }
1✔
204
        this.renderer.setStyle(this.placeholderNode, 'cssText', '');
205
        this.placeholderStyle = undefined;
206
        const styleObj = {
207
            width: this.placeholderNode.offsetWidth,
208
            height: this.fixedElement.nativeElement.offsetHeight
209
        };
210
        this.setAffixStyle(e, {
211
            ...this.affixStyle,
1✔
212
            ...styleObj
213
        });
214
        this.setPlaceholderStyle(styleObj);
215
    }
216

217
    updatePosition(e: Event): void {
218
        if (!this.platform.isBrowser) {
219
            return;
1✔
220
        }
221

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

291
        if (e.type === 'resize') {
292
            this.syncPlaceholderStyle(e);
293
        }
294
    }
295
}
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