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

atinc / ngx-tethys / e62d3b10-1466-49c3-aabd-707148681fc8

14 Jun 2024 08:24AM UTC coverage: 90.422%. Remained the same
e62d3b10-1466-49c3-aabd-707148681fc8

push

circleci

minlovehua
feat: use the ngx-tethys/util's coerceBooleanProperty instead of booleanAttribute #INFR-12648

5467 of 6692 branches covered (81.69%)

Branch coverage included in aggregate %.

117 of 120 new or added lines in 66 files covered. (97.5%)

183 existing lines in 46 files now uncovered.

13216 of 13970 relevant lines covered (94.6%)

985.91 hits per line

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

97.4
/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 { NgIf, NgTemplateOutlet } from '@angular/common';
8
import {
9
    ChangeDetectionStrategy,
10
    ChangeDetectorRef,
11
    Component,
12
    ContentChild,
13
    ElementRef,
14
    EventEmitter,
15
    HostBinding,
16
    Input,
1✔
17
    NgZone,
18
    numberAttribute,
270✔
19
    OnChanges,
20
    OnDestroy,
21
    OnInit,
47✔
22
    Output,
47✔
23
    SimpleChanges,
47✔
24
    TemplateRef,
47✔
25
    ViewChild
47✔
26
} from '@angular/core';
47✔
27

47✔
28
import { ThyProperties } from './properties.component';
47✔
29
import { coerceBooleanProperty } from 'ngx-tethys/util';
47✔
30

47✔
31
export type ThyPropertyItemOperationTrigger = 'hover' | 'always';
47✔
32

47✔
33
/**
47✔
34
 * 属性组件
47✔
35
 * @name thy-property-item
36
 */
37
@Component({
47✔
38
    selector: 'thy-property-item',
47✔
39
    templateUrl: './property-item.component.html',
51✔
40
    host: {
51✔
41
        class: 'thy-property-item',
42
        '[class.thy-property-item-operational]': '!!operation',
43
        '[class.thy-property-item-operational-hover]': "thyOperationTrigger === 'hover'"
44
    },
52✔
45
    changeDetection: ChangeDetectionStrategy.OnPush,
22✔
46
    standalone: true,
47
    imports: [NgIf, ThyFlexibleText, NgTemplateOutlet]
48
})
30✔
49
export class ThyPropertyItem implements OnInit, OnChanges, OnDestroy {
30✔
50
    /**
30✔
51
     * 属性名称
30✔
52
     * @type sting
1✔
53
     * @default thyLabelText
1✔
54
     */
55
    @Input() thyLabelText: string;
56

57
    /**
58
     * 设置属性是否是可编辑的
40✔
59
     * @type sting
40✔
60
     * @default false
11✔
61
     */
62
    @Input({ transform: coerceBooleanProperty }) thyEditable: boolean;
40✔
63

40✔
64
    /**
65
     * 设置跨列的数量
66
     * @type number
67
     */
68
    @Input({ transform: numberAttribute }) thySpan: number = 1;
69

UNCOV
70
    /**
×
71
     * 设置属性操作现实触发方式,默认 always 一直显示
72
     * @type 'hover' | 'always'
73
     */
6✔
74
    @Input() thyOperationTrigger: ThyPropertyItemOperationTrigger = 'always';
75

76
    @Output() thyEditingChange: EventEmitter<boolean> = new EventEmitter<boolean>();
69✔
77

44✔
78
    /**
44✔
79
     * 属性名称自定义模板
22✔
80
     * @type TemplateRef
81
     */
22✔
82
    @ContentChild('label', { static: true }) label!: TemplateRef<void>;
83

84
    /**
6✔
85
     * 属性内容编辑模板,只有在 thyEditable 为 true 时生效
6✔
86
     * @type TemplateRef
87
     */
88
    @ContentChild('editor', { static: true }) editor!: TemplateRef<void>;
89

90
    /**
91
     * 操作区模板
1✔
92
     * @type TemplateRef
1✔
93
     */
1!
94
    @ContentChild('operation', { static: true }) operation!: TemplateRef<void>;
1✔
95

96
    /**
97
     * @private
1✔
98
     */
99
    @ViewChild('contentTemplate', { static: true }) content!: TemplateRef<void>;
100

101
    /**
102
     * @private
5✔
103
     */
104
    @ViewChild('item', { static: true }) itemContent: ElementRef<HTMLElement>;
105

2✔
106
    editing: boolean;
107

108
    changes$ = new Subject<SimpleChanges>();
2✔
109

110
    private destroy$ = new Subject();
111

112
    private eventDestroy$ = new Subject();
6✔
113

6✔
114
    private originOverlays: OverlayRef[] = [];
1✔
115

116
    private clickEventSubscription: Subscription;
117

5✔
118
    @HostBinding('style.grid-column')
119
    get gridColumn() {
120
        return `span ${Math.min(this.thySpan, this.parent.thyColumn)}`;
121
    }
122

47✔
123
    isVertical = false;
47✔
124

47✔
125
    constructor(
47✔
126
        private cdr: ChangeDetectorRef,
127
        private clickDispatcher: ThyClickDispatcher,
1✔
128
        private ngZone: NgZone,
129
        private overlayOutsideClickDispatcher: OverlayOutsideClickDispatcher,
130
        private parent: ThyProperties
131
    ) {
132
        this.originOverlays = [...this.overlayOutsideClickDispatcher._attachedOverlays] as OverlayRef[];
133
    }
134

1✔
135
    ngOnInit() {
136
        this.subscribeClick();
137
        this.parent.layout$.pipe(takeUntil(this.destroy$)).subscribe(layout => {
138
            this.isVertical = layout === 'vertical';
139
            this.cdr.markForCheck();
140
        });
141
    }
142

143
    ngOnChanges(changes: SimpleChanges): void {
144
        if (changes.thyEditable && changes.thyEditable.currentValue) {
145
            this.subscribeClick();
146
        } else {
147
            this.setEditing(false);
148
            this.eventDestroy$.next();
1✔
149
            this.eventDestroy$.complete();
150

151
            if (this.clickEventSubscription) {
152
                this.clickEventSubscription.unsubscribe();
153
                this.clickEventSubscription = null;
154
            }
155
        }
156
    }
157

158
    setEditing(editing: boolean) {
159
        this.ngZone.run(() => {
160
            if (!!this.editing !== !!editing) {
161
                this.thyEditingChange.emit(editing);
162
            }
163
            this.editing = editing;
164
            this.cdr.markForCheck();
165
        });
166
    }
167

168
    /**
169
     * @deprecated please use setEditing(editing: boolean)
170
     */
171
    setKeepEditing(keep: boolean) {
172
        this.setEditing(keep);
173
    }
174

175
    private hasOverlay() {
176
        return this.overlayOutsideClickDispatcher._attachedOverlays.length > this.originOverlays.length;
177
    }
178

179
    private subscribeClick() {
180
        if (this.thyEditable === true) {
181
            this.ngZone.runOutsideAngular(() => {
182
                if (this.clickEventSubscription) {
183
                    return;
184
                }
185
                this.clickEventSubscription = fromEvent(this.itemContent.nativeElement, 'click')
186
                    .pipe(takeUntil(this.eventDestroy$))
187
                    .subscribe(() => {
188
                        this.setEditing(true);
189
                        this.bindEditorBlurEvent(this.itemContent.nativeElement);
190
                    });
191
            });
192
        }
193
    }
194

195
    private subscribeOverlayDetach() {
196
        const openedOverlays = this.overlayOutsideClickDispatcher._attachedOverlays.slice(this.originOverlays.length);
197
        const overlaysDetachments$ = openedOverlays.map(overlay => overlay.detachments());
198
        if (overlaysDetachments$.length) {
199
            combineLatest(overlaysDetachments$)
200
                .pipe(delay(50), take(1), takeUntil(this.destroy$))
201
                .subscribe(() => {
202
                    this.setEditing(false);
203
                });
204
        }
205
    }
206

207
    private subscribeDocumentClick(editorElement: HTMLElement) {
208
        this.clickDispatcher
209
            .clicked(0)
210
            .pipe(
211
                filter(event => {
212
                    return !editorElement.contains(event.target as HTMLElement);
213
                }),
214
                take(1),
215
                takeUntil(this.destroy$)
216
            )
217
            .subscribe(() => {
218
                this.setEditing(false);
219
            });
220
    }
221

222
    private bindEditorBlurEvent(editorElement: HTMLElement) {
223
        timer(0).subscribe(() => {
224
            if (this.hasOverlay()) {
225
                this.subscribeOverlayDetach();
226
            } else {
227
                this.subscribeDocumentClick(editorElement);
228
            }
229
        });
230
    }
231

232
    ngOnDestroy(): void {
233
        this.destroy$.next();
234
        this.destroy$.complete();
235

236
        this.eventDestroy$.next();
237
        this.eventDestroy$.complete();
238
    }
239
}
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