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

atinc / ngx-tethys / bccfbe82-aca8-4d24-b68f-0f535ef31f93

09 Jun 2025 07:26AM UTC coverage: 90.31% (+0.005%) from 90.305%
bccfbe82-aca8-4d24-b68f-0f535ef31f93

push

circleci

minlovehua
refactor: the TransormT type specified in input should be consistent with the transform function parameter type

5550 of 6819 branches covered (81.39%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 3 files covered. (100.0%)

20 existing lines in 3 files now uncovered.

13732 of 14532 relevant lines covered (94.49%)

903.3 hits per line

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

93.75
/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
import { OverlayOutsideClickDispatcher, OverlayRef } from '@angular/cdk/overlay';
6
import { NgTemplateOutlet } from '@angular/common';
7
import {
8
    ChangeDetectionStrategy,
9
    Component,
10
    ElementRef,
11
    NgZone,
12
    numberAttribute,
13
    OnDestroy,
14
    TemplateRef,
15
    inject,
16
    input,
17
    computed,
18
    effect,
1✔
19
    output,
20
    contentChild,
53✔
21
    viewChild,
53✔
22
    signal,
53✔
23
    DestroyRef
53✔
24
} from '@angular/core';
53✔
25
import { ThyProperties } from './properties.component';
53✔
26
import { coerceBooleanProperty, ThyBooleanInput } from 'ngx-tethys/util';
53✔
27
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
53✔
28

53✔
29
export type ThyPropertyItemOperationTrigger = 'hover' | 'always';
53✔
30

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

1✔
57
    /**
58
     * 属性名称
59
     */
60
    readonly thyLabelText = input<string>();
61

62
    /**
42✔
63
     * 设置属性是否是可编辑的
42✔
64
     */
13✔
65
    readonly thyEditable = input<boolean, ThyBooleanInput>(false, { transform: coerceBooleanProperty });
66

42✔
67
    /**
68
     * 设置跨列的数量
69
     */
70
    readonly thySpan = input(1, { transform: numberAttribute });
71

72
    /**
UNCOV
73
     * 设置编辑状态触发方法
×
74
     * @type 'hover' | 'click'
75
     */
76
    readonly thyEditTrigger = input<'hover' | 'click'>();
8✔
77

78
    /**
79
     * 设置属性操作现实触发方式,默认 always 一直显示
25!
80
     * @type 'hover' | 'always'
25✔
81
     */
25!
UNCOV
82
    readonly thyOperationTrigger = input<ThyPropertyItemOperationTrigger>('always');
×
83

84
    readonly thyEditingChange = output<boolean>();
25✔
85

25✔
86
    /**
87
     * 属性名称自定义模板
88
     * @type TemplateRef
8✔
89
     */
8✔
90
    readonly label = contentChild<TemplateRef<void>>('label');
8✔
91

92
    /**
93
     * 属性内容编辑模板,只有在 thyEditable 为 true 时生效
94
     * @type TemplateRef
95
     */
96
    readonly editor = contentChild<TemplateRef<void>>('editor');
1✔
97

1✔
98
    /**
1!
99
     * 操作区模板
1✔
100
     * @type TemplateRef
101
     */
102
    readonly operation = contentChild<TemplateRef<void>>('operation');
1✔
103

104
    /**
105
     * @private
106
     */
107
    readonly content = viewChild<TemplateRef<void>>('contentTemplate');
7✔
108

109
    /**
110
     * @private
2✔
111
     */
112
    readonly itemContent = viewChild<ElementRef<HTMLElement>>('item');
113

2✔
114
    editing = signal(false);
115

116
    private eventDestroy$ = new Subject<void>();
117

8✔
118
    private originOverlays: OverlayRef[] = [];
8✔
119

1✔
120
    private clickEventSubscription: Subscription;
121

122
    protected readonly gridColumn = computed(() => {
7✔
123
        return `span ${Math.min(this.thySpan(), this.parent?.thyColumn())}`;
124
    });
125

126
    readonly isVertical = computed(() => {
127
        return this.parent?.layout() === 'vertical';
53✔
128
    });
53✔
129

130
    constructor() {
1✔
131
        this.originOverlays = [...this.overlayOutsideClickDispatcher._attachedOverlays] as OverlayRef[];
1✔
132

133
        effect(() => {
134
            if (this.thyEditable()) {
135
                this.subscribeClick();
136
            } else {
137
                this.setEditing(false);
138
                this.eventDestroy$.next();
139
                this.eventDestroy$.complete();
140

141
                if (this.clickEventSubscription) {
142
                    this.clickEventSubscription.unsubscribe();
143
                    this.clickEventSubscription = null;
144
                }
145
            }
1✔
146
        });
147
    }
148

149
    setEditing(editing: boolean) {
150
        this.ngZone.run(() => {
151
            if (!!this.editing() !== !!editing) {
152
                this.thyEditingChange.emit(editing);
153
            }
154
            this.editing.set(editing);
155
        });
156
    }
157

158
    /**
159
     * @deprecated please use setEditing(editing: boolean)
160
     */
161
    setKeepEditing(keep: boolean) {
162
        this.setEditing(keep);
163
    }
164

165
    private hasOverlay() {
166
        return !!this.overlayOutsideClickDispatcher._attachedOverlays.filter(overlay => !this.originOverlays.includes(overlay)).length;
167
    }
168

169
    private subscribeClick() {
170
        if (this.thyEditable() === true) {
171
            this.ngZone.runOutsideAngular(() => {
172
                if (this.clickEventSubscription) {
173
                    return;
174
                }
175
                const itemElement = this.itemContent().nativeElement;
176
                this.clickEventSubscription = fromEvent(itemElement, 'click')
177
                    .pipe(takeUntil(this.eventDestroy$))
178
                    .subscribe(() => {
179
                        this.setEditing(true);
180
                        this.bindEditorBlurEvent(itemElement);
181
                        itemElement.querySelector('input')?.focus();
182
                    });
183
            });
184
        }
185
    }
186

187
    private subscribeOverlayDetach() {
188
        const openedOverlays = this.overlayOutsideClickDispatcher._attachedOverlays.filter(
189
            overlay => !this.originOverlays.includes(overlay)
190
        );
191
        const overlaysDetachments$ = openedOverlays.map(overlay => overlay.detachments());
192
        if (overlaysDetachments$.length) {
193
            combineLatest(overlaysDetachments$)
194
                .pipe(delay(50), take(1), takeUntilDestroyed(this.destroyRef))
195
                .subscribe(() => {
196
                    this.setEditing(false);
197
                });
198
        }
199
    }
200

201
    private subscribeDocumentClick(editorElement: HTMLElement) {
202
        this.clickDispatcher
203
            .clicked(0)
204
            .pipe(
205
                filter(event => {
206
                    return !editorElement.contains(event.target as HTMLElement);
207
                }),
208
                take(1),
209
                takeUntilDestroyed(this.destroyRef)
210
            )
211
            .subscribe(() => {
212
                this.setEditing(false);
213
            });
214
    }
215

216
    private bindEditorBlurEvent(editorElement: HTMLElement) {
217
        timer(0).subscribe(() => {
218
            if (this.hasOverlay()) {
219
                this.subscribeOverlayDetach();
220
            } else {
221
                this.subscribeDocumentClick(editorElement);
222
            }
223
        });
224
    }
225

226
    ngOnDestroy(): void {
227
        this.eventDestroy$.next();
228
        this.eventDestroy$.complete();
229
    }
230
}
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