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

atinc / ngx-tethys / ba7e05e2-37c0-44c6-8725-6f617aa0d43e

pending completion
ba7e05e2-37c0-44c6-8725-6f617aa0d43e

Pull #2756

circleci

huanhuanwa
test(color-picker): add test #INFR-8673
Pull Request #2756: feat(color-picker): add popoverRef param when panel open and close #INFR-8673

187 of 6315 branches covered (2.96%)

Branch coverage included in aggregate %.

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

2645 of 13660 relevant lines covered (19.36%)

83.2 hits per line

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

4.91
/src/affix/affix.component.ts
1
import { InputNumber, 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
    OnChanges,
1✔
18
    OnDestroy,
19
    Output,
×
20
    Renderer2,
×
21
    SimpleChanges,
22
    ViewChild,
23
    ViewEncapsulation
×
24
} from '@angular/core';
×
25

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

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

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

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

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

66
    /**
×
67
     * 距离窗口底部缓冲的偏移量阈值
×
68
     */
×
69
    @Input()
×
70
    @InputNumber()
×
71
    thyOffsetBottom?: null | number;
×
72

×
73
    /**
×
74
     * 固定状态改变时触发的回调函数
75
     */
76
    @Output() readonly thyChange = new EventEmitter<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);
×
84
    private destroy$ = new Subject<void>();
×
85
    private timeout?: number;
86
    private document: any;
×
87

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

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

×
106
    ngOnChanges(changes: SimpleChanges): void {
×
107
        const { thyOffsetBottom, thyOffsetTop, thyContainer } = changes;
108

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

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

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

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

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

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

×
152
        const scrollTop = this.scrollService.getScroll(target, true);
153
        const scrollLeft = this.scrollService.getScroll(target, false);
×
154

×
155
        const docElem = this.document.body;
×
156
        const clientTop = docElem.clientTop || 0;
×
157
        const clientLeft = docElem.clientLeft || 0;
×
158

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

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

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

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

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

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

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

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

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