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

atinc / ngx-tethys / f8ae789a-dc3f-4fe5-a240-550393566fc7

24 Jul 2024 03:33AM UTC coverage: 90.461% (-0.004%) from 90.465%
f8ae789a-dc3f-4fe5-a240-550393566fc7

push

circleci

minlovehua
feat(property): property operation position support behind label or content. #INFR-11955

5491 of 6714 branches covered (81.78%)

Branch coverage included in aggregate %.

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

2 existing lines in 2 files now uncovered.

13238 of 13990 relevant lines covered (94.62%)

997.42 hits per line

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

97.44
/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,
278✔
19
    OnChanges,
20
    OnDestroy,
21
    OnInit,
49✔
22
    Output,
49✔
23
    SimpleChanges,
49✔
24
    TemplateRef,
49✔
25
    ViewChild
49✔
26
} from '@angular/core';
49✔
27

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

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

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

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

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

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

76
    @Input() thyOperationBehindLabel = false;
77

71✔
78
    @Output() thyEditingChange: EventEmitter<boolean> = new EventEmitter<boolean>();
44✔
79

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

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

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

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

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

108
    editing: boolean;
109

2✔
110
    changes$ = new Subject<SimpleChanges>();
111

112
    private destroy$ = new Subject();
113

6✔
114
    private eventDestroy$ = new Subject();
6✔
115

1✔
116
    private originOverlays: OverlayRef[] = [];
117

118
    private clickEventSubscription: Subscription;
5✔
119

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

49✔
125
    isVertical = false;
49✔
126

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

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

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

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

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

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

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

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

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

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

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

234
    ngOnDestroy(): void {
235
        this.destroy$.next();
236
        this.destroy$.complete();
237

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