• 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

4.71
/src/property/property-item.component.ts
1
import { ThyClickDispatcher } from 'ngx-tethys/core';
2
import { ThyFlexibleText } from 'ngx-tethys/flexible-text';
3
import { combineLatest, fromEvent, Subject, Subscription, timer } from 'rxjs';
4
import { delay, filter, take, takeUntil } from 'rxjs/operators';
5

6
import { OverlayOutsideClickDispatcher, OverlayRef } from '@angular/cdk/overlay';
7
import { NgTemplateOutlet } from '@angular/common';
8
import {
9
    ChangeDetectionStrategy,
10
    Component,
11
    ElementRef,
12
    NgZone,
13
    numberAttribute,
14
    OnDestroy,
15
    OnInit,
16
    SimpleChanges,
17
    TemplateRef,
1✔
18
    inject,
UNCOV
19
    input,
×
UNCOV
20
    computed,
×
UNCOV
21
    effect,
×
UNCOV
22
    output,
×
UNCOV
23
    contentChild,
×
UNCOV
24
    viewChild,
×
UNCOV
25
    signal
×
UNCOV
26
} from '@angular/core';
×
UNCOV
27

×
UNCOV
28
import { ThyProperties } from './properties.component';
×
UNCOV
29
import { coerceBooleanProperty, ThyBooleanInput } from 'ngx-tethys/util';
×
UNCOV
30

×
UNCOV
31
export type ThyPropertyItemOperationTrigger = 'hover' | 'always';
×
UNCOV
32

×
UNCOV
33
/**
×
UNCOV
34
 * 属性组件
×
UNCOV
35
 * @name thy-property-item
×
UNCOV
36
 */
×
UNCOV
37
@Component({
×
UNCOV
38
    selector: 'thy-property-item',
×
UNCOV
39
    templateUrl: './property-item.component.html',
×
UNCOV
40
    host: {
×
41
        class: 'thy-property-item',
UNCOV
42
        '[class.thy-property-edit-trigger-hover]': 'thyEditTrigger() === "hover"',
×
UNCOV
43
        '[class.thy-property-edit-trigger-click]': 'thyEditTrigger() === "click"',
×
UNCOV
44
        '[class.thy-property-item-operational]': '!!operation()',
×
UNCOV
45
        '[class.thy-property-item-operational-hover]': "thyOperationTrigger() === 'hover'",
×
UNCOV
46
        '[style.grid-column]': 'gridColumn()',
×
47
        '[class.thy-property-item-single]': '!parent'
48
    },
UNCOV
49
    changeDetection: ChangeDetectionStrategy.OnPush,
×
UNCOV
50
    imports: [ThyFlexibleText, NgTemplateOutlet]
×
UNCOV
51
})
×
UNCOV
52
export class ThyPropertyItem implements OnDestroy {
×
UNCOV
53
    private clickDispatcher = inject(ThyClickDispatcher);
×
UNCOV
54
    private ngZone = inject(NgZone);
×
55
    private overlayOutsideClickDispatcher = inject(OverlayOutsideClickDispatcher);
56
    private parent = inject(ThyProperties, { optional: true });
57

UNCOV
58
    /**
×
UNCOV
59
     * 属性名称
×
UNCOV
60
     * @type sting
×
61
     * @default thyLabelText
62
     */
63
    readonly thyLabelText = input<string>();
UNCOV
64

×
UNCOV
65
    /**
×
UNCOV
66
     * 设置属性是否是可编辑的
×
67
     * @type sting
UNCOV
68
     * @default false
×
69
     */
70
    readonly thyEditable = input<boolean, ThyBooleanInput>(false, { transform: coerceBooleanProperty });
71

72
    /**
73
     * 设置跨列的数量
74
     * @type number
75
     */
×
76
    readonly thySpan = input(1, { transform: numberAttribute });
77

UNCOV
78
    /**
×
79
     * 设置编辑状态触发方法
80
     * @type 'hover' | 'click'
UNCOV
81
     */
×
UNCOV
82
    readonly thyEditTrigger = input<'hover' | 'click'>();
×
UNCOV
83

×
84
    /**
×
85
     * 设置属性操作现实触发方式,默认 always 一直显示
UNCOV
86
     * @type 'hover' | 'always'
×
UNCOV
87
     */
×
88
    readonly thyOperationTrigger = input<ThyPropertyItemOperationTrigger>('always');
89

UNCOV
90
    readonly thyEditingChange = output<boolean>();
×
UNCOV
91

×
UNCOV
92
    /**
×
93
     * 属性名称自定义模板
94
     * @type TemplateRef
95
     */
96
    readonly label = contentChild<TemplateRef<void>>('label');
97

UNCOV
98
    /**
×
UNCOV
99
     * 属性内容编辑模板,只有在 thyEditable 为 true 时生效
×
UNCOV
100
     * @type TemplateRef
×
UNCOV
101
     */
×
102
    readonly editor = contentChild<TemplateRef<void>>('editor');
103

UNCOV
104
    /**
×
105
     * 操作区模板
106
     * @type TemplateRef
107
     */
108
    readonly operation = contentChild<TemplateRef<void>>('operation');
UNCOV
109

×
110
    /**
111
     * @private
UNCOV
112
     */
×
113
    readonly content = viewChild<TemplateRef<void>>('contentTemplate');
114

UNCOV
115
    /**
×
116
     * @private
117
     */
118
    readonly itemContent = viewChild<ElementRef<HTMLElement>>('item');
UNCOV
119

×
UNCOV
120
    editing = signal(false);
×
UNCOV
121

×
122
    changes$ = new Subject<SimpleChanges>();
123

UNCOV
124
    private destroy$ = new Subject<void>();
×
125

126
    private eventDestroy$ = new Subject<void>();
127

128
    private originOverlays: OverlayRef[] = [];
UNCOV
129

×
UNCOV
130
    private clickEventSubscription: Subscription;
×
UNCOV
131

×
UNCOV
132
    protected readonly gridColumn = computed(() => {
×
133
        return `span ${Math.min(this.thySpan(), this.parent?.thyColumn())}`;
134
    });
1✔
135

1✔
136
    isVertical = signal(false);
137

138
    constructor() {
139
        this.originOverlays = [...this.overlayOutsideClickDispatcher._attachedOverlays] as OverlayRef[];
140

141
        effect(() => {
142
            if (this.thyEditable()) {
143
                this.subscribeClick();
144
            } else {
145
                this.setEditing(false);
146
                this.eventDestroy$.next();
147
                this.eventDestroy$.complete();
148

149
                if (this.clickEventSubscription) {
1✔
150
                    this.clickEventSubscription.unsubscribe();
151
                    this.clickEventSubscription = null;
152
                }
153
            }
154
        });
155

156
        effect(() => {
157
            const layout = this.parent?.layout();
158
            this.isVertical.set(layout === 'vertical');
159
        });
160
    }
161

162
    setEditing(editing: boolean) {
163
        this.ngZone.run(() => {
164
            if (!!this.editing() !== !!editing) {
165
                this.thyEditingChange.emit(editing);
166
            }
167
            this.editing.set(editing);
168
        });
169
    }
170

171
    /**
172
     * @deprecated please use setEditing(editing: boolean)
173
     */
174
    setKeepEditing(keep: boolean) {
175
        this.setEditing(keep);
176
    }
177

178
    private hasOverlay() {
179
        return !!this.overlayOutsideClickDispatcher._attachedOverlays.filter(overlay => !this.originOverlays.includes(overlay)).length;
180
    }
181

182
    private subscribeClick() {
183
        if (this.thyEditable() === true) {
184
            this.ngZone.runOutsideAngular(() => {
185
                if (this.clickEventSubscription) {
186
                    return;
187
                }
188
                const itemElement = this.itemContent().nativeElement;
189
                this.clickEventSubscription = fromEvent(itemElement, 'click')
190
                    .pipe(takeUntil(this.eventDestroy$))
191
                    .subscribe(() => {
192
                        this.setEditing(true);
193
                        this.bindEditorBlurEvent(itemElement);
194
                        itemElement.querySelector('input')?.focus();
195
                    });
196
            });
197
        }
198
    }
199

200
    private subscribeOverlayDetach() {
201
        const openedOverlays = this.overlayOutsideClickDispatcher._attachedOverlays.filter(
202
            overlay => !this.originOverlays.includes(overlay)
203
        );
204
        const overlaysDetachments$ = openedOverlays.map(overlay => overlay.detachments());
205
        if (overlaysDetachments$.length) {
206
            combineLatest(overlaysDetachments$)
207
                .pipe(delay(50), take(1), takeUntil(this.destroy$))
208
                .subscribe(() => {
209
                    this.setEditing(false);
210
                });
211
        }
212
    }
213

214
    private subscribeDocumentClick(editorElement: HTMLElement) {
215
        this.clickDispatcher
216
            .clicked(0)
217
            .pipe(
218
                filter(event => {
219
                    return !editorElement.contains(event.target as HTMLElement);
220
                }),
221
                take(1),
222
                takeUntil(this.destroy$)
223
            )
224
            .subscribe(() => {
225
                this.setEditing(false);
226
            });
227
    }
228

229
    private bindEditorBlurEvent(editorElement: HTMLElement) {
230
        timer(0).subscribe(() => {
231
            if (this.hasOverlay()) {
232
                this.subscribeOverlayDetach();
233
            } else {
234
                this.subscribeDocumentClick(editorElement);
235
            }
236
        });
237
    }
238

239
    ngOnDestroy(): void {
240
        this.destroy$.next();
241
        this.destroy$.complete();
242

243
        this.eventDestroy$.next();
244
        this.eventDestroy$.complete();
245
    }
246
}
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