• 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

2.48
/src/popover/popover.service.ts
1
import { ComponentTypeOrTemplateRef, getFlexiblePositions, ThyAbstractOverlayRef, ThyAbstractOverlayService } from 'ngx-tethys/core';
2
import { FunctionProp, isFunction } from 'ngx-tethys/util';
3
import { of, Subject } from 'rxjs';
4
import { takeUntil } from 'rxjs/operators';
5

6
import { Directionality } from '@angular/cdk/bidi';
7
import { coerceArray, coerceElement } from '@angular/cdk/coercion';
8
import {
9
    FlexibleConnectedPositionStrategy,
10
    FlexibleConnectedPositionStrategyOrigin,
11
    Overlay,
12
    OverlayConfig,
13
    OverlayContainer,
14
    OverlayRef,
15
    PositionStrategy,
16
    ScrollStrategy,
17
    ViewportRuler
18
} from '@angular/cdk/overlay';
19
import { Platform } from '@angular/cdk/platform';
20
import { ComponentPortal } from '@angular/cdk/portal';
21
import { DOCUMENT } from '@angular/common';
1✔
22
import { ElementRef, Injectable, Injector, NgZone, OnDestroy, StaticProvider, TemplateRef, inject } from '@angular/core';
UNCOV
23

×
24
import { ThyPopoverContainer } from './popover-container.component';
UNCOV
25
import { ThyInternalPopoverRef, ThyPopoverRef } from './popover-ref';
×
UNCOV
26
import {
×
UNCOV
27
    THY_POPOVER_DEFAULT_CONFIG,
×
UNCOV
28
    THY_POPOVER_DEFAULT_CONFIG_VALUE,
×
UNCOV
29
    THY_POPOVER_SCROLL_STRATEGY,
×
UNCOV
30
    ThyPopoverConfig
×
UNCOV
31
} from './popover.config';
×
UNCOV
32
import { popoverAbstractOverlayOptions } from './popover.options';
×
33
import { SafeAny } from 'ngx-tethys/types';
34

35
/**
×
36
 * @public
37
 * @order 10
UNCOV
38
 */
×
39
@Injectable()
40
export class ThyPopover extends ThyAbstractOverlayService<ThyPopoverConfig, ThyPopoverContainer> implements OnDestroy {
UNCOV
41
    private ngZone = inject(NgZone);
×
UNCOV
42
    private _viewportRuler = inject(ViewportRuler);
×
43
    private _document = inject(DOCUMENT, { optional: true })!;
UNCOV
44
    private _platform = inject(Platform);
×
UNCOV
45
    private _overlayContainer = inject(OverlayContainer);
×
46

47
    private readonly ngUnsubscribe$ = new Subject();
48

×
49
    private originInstancesMap = new Map<
50
        ElementRef | HTMLElement,
51
        {
UNCOV
52
            config: ThyPopoverConfig;
×
UNCOV
53
            popoverRef: ThyPopoverRef<any, any>;
×
UNCOV
54
        }
×
UNCOV
55
    >();
×
UNCOV
56

×
57
    private buildPositionStrategy<TData>(config: ThyPopoverConfig<TData>): PositionStrategy {
58
        const origin: FlexibleConnectedPositionStrategyOrigin = config.originPosition ? config.originPosition : config.origin;
UNCOV
59
        // const positionStrategy = this.overlay.position().flexibleConnectedTo(origin);
×
UNCOV
60
        const positionStrategy = new FlexibleConnectedPositionStrategy(
×
61
            origin,
×
62
            this._viewportRuler,
63
            this._document,
UNCOV
64
            this._platform,
×
UNCOV
65
            this._overlayContainer
×
UNCOV
66
        );
×
67
        const positions = getFlexiblePositions(config.placement, config.offset, 'thy-popover');
68
        positionStrategy.withPositions(positions);
UNCOV
69
        positionStrategy.withPush(config.canPush);
×
70
        positionStrategy.withGrowAfterOpen(true);
71
        positionStrategy.withTransformOriginOn('.thy-popover-container');
UNCOV
72
        positionStrategy.positionChanges.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(change => {
×
UNCOV
73
            if (change.scrollableViewProperties.isOverlayClipped) {
×
74
                // After position changes occur and the overlay is clipped by
75
                // a parent scrollable then close the tooltip.
76
                this.ngZone.run(() => this.close());
77
            }
78
        });
79
        return positionStrategy;
UNCOV
80
    }
×
81

×
82
    private buildScrollStrategy(config: ThyPopoverConfig): ScrollStrategy {
83
        if (config.scrollStrategy) {
84
            return config.scrollStrategy;
85
        } else if (this.scrollStrategy && isFunction(this.scrollStrategy)) {
86
            return this.scrollStrategy();
87
        } else {
88
            this.overlay.scrollStrategies.block();
UNCOV
89
        }
×
90
    }
91

UNCOV
92
    protected buildOverlayConfig<TData>(config: ThyPopoverConfig<TData>): OverlayConfig {
×
UNCOV
93
        const positionStrategy = this.buildPositionStrategy(config);
×
94
        const overlayConfig = this.buildBaseOverlayConfig(config, 'thy-popover-panel');
95
        overlayConfig.positionStrategy = positionStrategy;
96
        overlayConfig.scrollStrategy = this.buildScrollStrategy(config);
UNCOV
97
        return overlayConfig;
×
UNCOV
98
    }
×
99

100
    protected attachOverlayContainer(overlay: OverlayRef, config: ThyPopoverConfig<any>): ThyPopoverContainer {
101
        const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
UNCOV
102
        const injector = Injector.create({
×
UNCOV
103
            parent: userInjector || this.injector,
×
UNCOV
104
            providers: [{ provide: ThyPopoverConfig, useValue: config }]
×
UNCOV
105
        });
×
UNCOV
106
        const containerPortal = new ComponentPortal(ThyPopoverContainer, config.viewContainerRef, injector);
×
107
        const containerRef = overlay.attach<ThyPopoverContainer>(containerPortal);
108
        return containerRef.instance;
109
    }
UNCOV
110

×
UNCOV
111
    protected createAbstractOverlayRef<T, TResult = unknown>(
×
UNCOV
112
        overlayRef: OverlayRef,
×
UNCOV
113
        containerInstance: ThyPopoverContainer,
×
UNCOV
114
        config: ThyPopoverConfig
×
UNCOV
115
    ): ThyAbstractOverlayRef<T, ThyPopoverContainer, TResult> {
×
UNCOV
116
        return new ThyInternalPopoverRef(overlayRef, containerInstance, config);
×
117
    }
118

UNCOV
119
    protected createInjector<T>(config: ThyPopoverConfig, popoverRef: ThyPopoverRef<T>, popoverContainer: ThyPopoverContainer): Injector {
×
UNCOV
120
        const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
×
UNCOV
121

×
UNCOV
122
        const injectionTokens: StaticProvider[] = [
×
UNCOV
123
            { provide: ThyPopoverContainer, useValue: popoverContainer },
×
UNCOV
124
            {
×
125
                provide: ThyPopoverRef,
126
                useValue: popoverRef
127
            }
UNCOV
128
        ];
×
UNCOV
129

×
130
        if (config.direction && (!userInjector || !userInjector.get<Directionality | null>(Directionality, null))) {
UNCOV
131
            injectionTokens.push({
×
132
                provide: Directionality,
133
                useValue: {
UNCOV
134
                    value: config.direction,
×
135
                    change: of()
136
                }
137
            });
138
        }
139

UNCOV
140
        return Injector.create({ parent: userInjector || this.injector, providers: injectionTokens });
×
141
    }
142

143
    private originElementAddActiveClass(config: ThyPopoverConfig) {
UNCOV
144
        if (config.originActiveClass) {
×
UNCOV
145
            coerceElement<HTMLElement>(config.origin).classList.add(...coerceArray(config.originActiveClass));
×
146
        }
UNCOV
147
    }
×
UNCOV
148

×
UNCOV
149
    private originElementRemoveActiveClass(config: ThyPopoverConfig) {
×
UNCOV
150
        if (config.originActiveClass) {
×
UNCOV
151
            coerceElement<HTMLElement>(config.origin).classList.remove(...coerceArray(config.originActiveClass));
×
152
        }
UNCOV
153
    }
×
UNCOV
154

×
155
    constructor() {
156
        const overlay = inject(Overlay);
157
        const injector = inject(Injector);
UNCOV
158
        const defaultConfig = inject(THY_POPOVER_DEFAULT_CONFIG, { optional: true })!;
×
159
        const scrollStrategy = inject<FunctionProp<ScrollStrategy>>(THY_POPOVER_SCROLL_STRATEGY);
160

161
        super(
162
            popoverAbstractOverlayOptions,
163
            overlay,
UNCOV
164
            injector,
×
165
            {
166
                ...THY_POPOVER_DEFAULT_CONFIG_VALUE,
167
                ...defaultConfig
168
            },
169
            scrollStrategy
UNCOV
170
        );
×
171
    }
172

173
    private ensureCloseClosest(origin: HTMLElement) {
174
        let closeAndEnd = false;
175
        this.originInstancesMap.forEach((value, key) => {
176
            if (value.config.manualClosure) {
UNCOV
177
                if (key === origin) {
×
UNCOV
178
                    value.popoverRef.close();
×
UNCOV
179
                    closeAndEnd = true;
×
180
                }
UNCOV
181
            } else {
×
182
                if (key === origin) {
183
                    closeAndEnd = true;
184
                }
185
                value.popoverRef.close();
186
            }
UNCOV
187
        });
×
188
        return closeAndEnd;
189
    }
190

191
    /**
192
     * 打开悬浮层
UNCOV
193
     */
×
194
    open<T, TData = unknown, TResult = unknown>(
195
        componentOrTemplateRef: ComponentTypeOrTemplateRef<T>,
1✔
196
        config?: ThyPopoverConfig<TData>
197
    ): ThyPopoverRef<T, TResult> {
1✔
198
        const originElement = coerceElement(config.origin);
199
        // 默认关闭之前的弹出框
200
        // 1. 当之前的 Popover 设置 manualClosure 为 true 时, 弹出其他 Popover 时不自动关闭 manualClosure 为 true 的 Popover
201
        // 2. 当前的 Origin 对应的 Popover 已经弹出,不管 manualClosure 设置为何,直接关闭并返回
202
        if (this.ensureCloseClosest(originElement)) {
203
            return;
204
        }
205

206
        const popoverRef = this.openOverlay<T, TResult>(componentOrTemplateRef, config) as ThyPopoverRef<T, TResult, TData>;
207
        config = popoverRef.containerInstance.config;
208
        popoverRef.afterClosed().subscribe(() => {
209
            this.originElementRemoveActiveClass(config);
210
            this.originInstancesMap.delete(originElement);
211
        });
212

213
        this.originElementAddActiveClass(config);
214
        this.originInstancesMap.set(originElement, {
215
            config,
216
            popoverRef
217
        });
218

219
        return popoverRef;
220
    }
221

222
    /**
223
     * 根据Id获取打开的悬浮层
224
     */
225
    getPopoverById(id: string): ThyPopoverRef<any> | undefined {
226
        return this.getAbstractOverlayById(id) as ThyPopoverRef<any> | undefined;
227
    }
228

229
    /**
230
     * 获取已经打开的悬浮层
231
     */
232
    getOpenedPopovers(): ThyPopoverRef<SafeAny>[] {
233
        return this.getAbstractOverlays();
234
    }
235

236
    /**
237
     * @description.en-us Finds the closest ThyPopoverRef to an element by looking at the DOM.
238
     * @description 通过 Dom 元素查找最近弹出的悬浮层
239
     */
240
    getClosestPopover(element: HTMLElement): ThyPopoverRef<any> | undefined {
241
        const parent = element.closest('.thy-popover-container');
242

243
        if (parent && parent.id) {
244
            return this.getPopoverById(parent.id);
245
        }
246
        return null;
247
    }
248

249
    /**
250
     * 关闭悬浮层
251
     */
252
    close<T>(result?: T, force?: boolean) {
253
        super.close(result, force);
254
    }
255

256
    /**
257
     * @internal
258
     */
259
    ngOnDestroy() {
260
        this.dispose();
261
    }
262
}
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