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

atinc / ngx-tethys / 1a2b40f4-d0f1-4a92-b98b-b0c5ec4535e4

21 May 2025 02:14AM UTC coverage: 90.22%. Remained the same
1a2b40f4-d0f1-4a92-b98b-b0c5ec4535e4

push

circleci

web-flow
feat(colorPicker): migrate to signal for colorPicker (#3450)

5560 of 6827 branches covered (81.44%)

Branch coverage included in aggregate %.

60 of 67 new or added lines in 8 files covered. (89.55%)

5 existing lines in 3 files now uncovered.

13646 of 14461 relevant lines covered (94.36%)

902.91 hits per line

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

89.29
/src/color-picker/color-picker.component.ts
1
import { FocusMonitor } from '@angular/cdk/a11y';
2
import { OverlayRef } from '@angular/cdk/overlay';
3
import { Platform } from '@angular/cdk/platform';
4
import {
5
    AfterViewInit,
6
    Directive,
7
    effect,
8
    ElementRef,
9
    forwardRef,
10
    inject,
11
    Input,
12
    input,
13
    NgZone,
14
    numberAttribute,
15
    OnDestroy,
16
    OnInit,
17
    output
19✔
18
} from '@angular/core';
19✔
19
import { NG_VALUE_ACCESSOR } from '@angular/forms';
19✔
20
import { mixinDisabled, mixinTabIndex, ThyOverlayDirectiveBase, ThyOverlayTrigger, ThyPlacement } from 'ngx-tethys/core';
21
import { ThyPopover, ThyPopoverRef } from 'ngx-tethys/popover';
22
import { coerceBooleanProperty } from 'ngx-tethys/util';
23
import { fromEvent, Subject } from 'rxjs';
24
import { takeUntil } from 'rxjs/operators';
1✔
25
import { ThyColorPickerPanel } from './color-picker-panel.component';
26
import { DEFAULT_COLORS } from './constant';
27
import { ThyColor } from './helpers/color.class';
28

29
export class OverlayBase extends ThyOverlayDirectiveBase {
1✔
30
    constructor(
31
        protected zone: NgZone,
19✔
32
        protected elementRef: ElementRef<HTMLElement>,
33
        platform: Platform,
34
        focusMonitor: FocusMonitor
124✔
35
    ) {
36
        super(elementRef, platform, focusMonitor, zone, true);
37
    }
1✔
38

39
    show(): void {}
40

19✔
41
    hide() {}
19✔
42
}
19✔
43

19✔
44
const _BaseMixin = mixinTabIndex(mixinDisabled(OverlayBase));
19✔
45

19✔
46
/**
19✔
47
 * 颜色选择组件
19✔
48
 * @name thyColorPicker
19✔
49
 */
19✔
50
@Directive({
19✔
51
    selector: '[thyColorPicker]',
19✔
52
    host: {
19✔
53
        class: 'thy-color-picker',
19✔
54
        '[attr.tabindex]': `tabIndex`,
19✔
55
        '[class.thy-color-picker-disabled]': 'disabled'
19✔
56
    },
19✔
57
    providers: [
19✔
58
        {
19✔
59
            provide: NG_VALUE_ACCESSOR,
19✔
60
            multi: true,
19✔
61
            useExisting: forwardRef(() => ThyColorPickerDirective)
19✔
62
        }
63
    ]
18!
64
})
18!
65
export class ThyColorPickerDirective extends _BaseMixin implements OnInit, OnDestroy, AfterViewInit {
18!
66
    private thyPopover = inject(ThyPopover);
67

68
    /**
69
     * 弹框偏移量
70
     * @type  number
18✔
71
     */
18✔
72
    readonly thyOffset = input<number, unknown>(0, { transform: numberAttribute });
3✔
73

3✔
74
    /**
75
     * 颜色选择面板是否有幕布
76
     */
4!
77
    readonly thyHasBackdrop = input(true, { transform: coerceBooleanProperty });
4✔
78

79
    /**
80
     * 设置颜色选择面板的默认颜色选项值
81
     */
2✔
82
    readonly thyDefaultColor = input<string>();
83

84
    /**
2✔
85
     * 是否显示'无填充色'选项
2✔
86
     */
87
    readonly thyTransparentColorSelectable = input(true, { transform: coerceBooleanProperty });
88

89
    /**
90
     * 预设的快捷选择颜色
91
     * @type string[]
92
     */
93
    readonly thyPresetColors = input<string[]>(DEFAULT_COLORS);
15✔
94

15✔
95
    /**
96
     * 颜色面板弹出位置 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom'
97
     * @type ThyPlacement
98
     */
99
    readonly thyPlacement = input<ThyPlacement>('bottom');
100

101
    /**
102
     * panel 展开后触发
103
     */
104
    readonly thyPanelOpen = output<ThyPopoverRef<ThyColorPickerPanel>>();
10✔
105

4✔
106
    /**
107
     * panel 关闭后触发
6✔
108
     */
109
    readonly thyPanelClose = output<ThyPopoverRef<ThyColorPickerPanel>>();
110

111
    /**
112
     * 弹出悬浮层的触发方式
113
     * @type 'hover' | 'click'
114
     */
NEW
115
    readonly thyTrigger = input<ThyOverlayTrigger>('click');
×
UNCOV
116

×
117
    /**
118
     * 显示延迟时间
119
     */
120
    readonly thyShowDelay = input<number, unknown>(100, { transform: numberAttribute });
15!
121

15✔
122
    /**
12✔
123
     * 隐藏延迟时间
124
     */
15✔
125
    readonly thyHideDelay = input<number, unknown>(100, { transform: numberAttribute });
15✔
126

15✔
127
    /**
128
     * 是否属于禁用状态
129
     */
15✔
130
    @Input({ transform: coerceBooleanProperty })
4✔
131
    override set thyDisabled(value: boolean) {
132
        this.disabled = value;
133
    }
134
    override get thyDisabled(): boolean {
2✔
135
        return this.disabled;
1✔
136
    }
137

1!
138
    protected onChangeFn: (value: number | string) => void = () => {};
1✔
139

140
    private onTouchFn: () => void = () => {};
141

142
    color: string;
15✔
143

144
    private popoverRef: ThyPopoverRef<ThyColorPickerPanel>;
16✔
145

16!
UNCOV
146
    private closePanel = true;
×
UNCOV
147

×
148
    private destroy$ = new Subject<void>();
149

16✔
150
    public get backgroundColor(): string {
1✔
151
        return this.color;
152
    }
15✔
153

12✔
154
    constructor() {
155
        const zone = inject(NgZone);
15✔
156
        const elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
15✔
157
        const platform = inject(Platform);
15✔
158
        const focusMonitor = inject(FocusMonitor);
15✔
159

160
        super(zone, elementRef, platform, focusMonitor);
161

21✔
162
        effect(() => {
21!
NEW
163
            // TODO: 基类参数
×
NEW
164
            this.showDelay = this.thyShowDelay() ?? 100;
×
165
            this.hideDelay = this.thyHideDelay() ?? 100;
166
            this.trigger = this.thyTrigger() || 'click';
21✔
167
        });
2!
168
    }
2✔
169

170
    ngOnInit(): void {}
2✔
171

172
    ngAfterViewInit() {
173
        this.initialize();
174
        if (this.trigger === 'hover') {
36✔
175
            this.ngZone.runOutsideAngular(() => {
176
                return fromEvent(document, 'mousemove')
177
                    .pipe(takeUntil(this.destroy$))
18✔
178
                    .subscribe(event => {
179
                        if (this.popoverRef?.getOverlayRef()?.hasAttached()) {
180
                            if (
18✔
181
                                this.elementRef.nativeElement.contains(event.target as HTMLElement) ||
182
                                (event.target as HTMLElement).closest('.thy-color-picker-custom-panel') ||
183
                                !!(event.target as HTMLElement).querySelector('.thy-color-picker-custom-panel') ||
20✔
184
                                this.popoverRef.getOverlayRef()?.hostElement?.contains(event.target as HTMLElement)
185
                            ) {
186
                                this.closePanel = false;
1✔
187
                            } else {
1✔
188
                                this.closePanel = true;
189
                                this.popoverRef.close();
190
                            }
19✔
191
                        }
19✔
192
                    });
19✔
193
            });
19✔
194
        }
19✔
195
    }
196

1✔
197
    togglePanel(): OverlayRef {
1✔
198
        this.closePanel = false;
199
        this.popoverRef = this.thyPopover.open(ThyColorPickerPanel, {
200
            origin: this.elementRef.nativeElement as HTMLElement,
201
            offset: this.thyOffset(),
202
            manualClosure: true,
203
            width: '286px',
204
            placement: this.thyPlacement(),
205
            originActiveClass: 'thy-default-picker-active',
206
            hasBackdrop: this.thyHasBackdrop(),
207
            outsideClosable: false,
208
            canClose: () => {
209
                if (this.trigger === 'hover') {
210
                    return this.closePanel;
211
                }
212
                return true;
1✔
213
            },
214
            initialState: {
215
                color: new ThyColor(this.color).toHexString(true),
216
                defaultColor: this.thyDefaultColor(),
217
                transparentColorSelectable: this.thyTransparentColorSelectable(),
218
                defaultColors: this.thyPresetColors(),
219
                colorChange: (value: string) => {
220
                    this.closePanel = true;
221
                    this.onModelChange(value);
222
                }
223
            }
224
        });
19✔
225
        if (this.popoverRef) {
226
            this.popoverRef.afterOpened().subscribe(() => {
227
                this.thyPanelOpen.emit(this.popoverRef);
228
            });
229
            this.popoverRef.afterClosed().subscribe(() => {
230
                this.thyPanelClose.emit(this.popoverRef);
231
                this.elementRef.nativeElement.focus();
232
            });
233
        }
234
        if (this.popoverRef && !this.thyHasBackdrop()) {
235
            this.popoverRef
236
                .getOverlayRef()
237
                .outsidePointerEvents()
238
                .subscribe(event => {
239
                    if ((event.target as HTMLElement).closest('.thy-color-picker-custom-panel')) {
240
                        return;
241
                    }
242
                    if (!this.popoverRef.getOverlayRef().hostElement.contains(event.target as HTMLElement)) {
243
                        this.popoverRef.close();
244
                    }
245
                });
246
        }
247
        return this.popoverRef.getOverlayRef();
248
    }
249

250
    override show(delay: number = this.showDelay): void {
251
        if (this.hideTimeoutId) {
252
            clearTimeout(this.hideTimeoutId);
253
            this.hideTimeoutId = null;
254
        }
255

256
        if (this.disabled || (this.overlayRef && this.overlayRef.hasAttached())) {
257
            return;
258
        }
259
        if (this.trigger !== 'hover') {
260
            delay = 0;
261
        }
262

263
        this.showTimeoutId = setTimeout(() => {
264
            const overlayRef = this.togglePanel();
265
            this.overlayRef = overlayRef;
266
            this.showTimeoutId = null;
267
        }, delay);
268
    }
269

270
    override hide(delay: number = this.hideDelay) {
271
        if (this.showTimeoutId) {
272
            clearTimeout(this.showTimeoutId);
273
            this.showTimeoutId = null;
274
        }
275

276
        this.hideTimeoutId = setTimeout(() => {
277
            if (this.popoverRef) {
278
                this.popoverRef?.getOverlayRef()?.hasAttached() && this.popoverRef.close();
279
            }
280
            this.hideTimeoutId = null;
281
        }, delay);
282
    }
283

284
    writeValue(value: string): void {
285
        this.color = value;
286
    }
287

288
    registerOnChange(fn: any): void {
289
        this.onChangeFn = fn;
290
    }
291

292
    registerOnTouched(fn: any): void {
293
        this.onTouchFn = fn;
294
    }
295

296
    setDisabledState?(isDisabled: boolean): void {
297
        this.disabled = isDisabled;
298
    }
299

300
    onModelChange(value: string): void {
301
        this.color = value;
302
        this.onChangeFn(value);
303
    }
304

305
    ngOnDestroy(): void {
306
        this.destroy$.next();
307
        this.destroy$.complete();
308
        this.hide();
309
        this.dispose();
310
        this.popoverRef = null;
311
    }
312
}
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