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

atinc / ngx-tethys / d9ae709b-3c27-4b69-b125-b8b80b54f90b

pending completion
d9ae709b-3c27-4b69-b125-b8b80b54f90b

Pull #2757

circleci

mengshuicmq
fix: fix code review
Pull Request #2757: feat(color-picker): color-picker support disabled (#INFR-8645)

98 of 6315 branches covered (1.55%)

Branch coverage included in aggregate %.

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

2392 of 13661 relevant lines covered (17.51%)

83.12 hits per line

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

3.91
/src/autocomplete/autocomplete.trigger.directive.ts
1
import {
2
    Directive,
3
    ElementRef,
4
    Injectable,
5
    NgZone,
6
    OnDestroy,
7
    Input,
8
    OnInit,
9
    ViewContainerRef,
10
    HostBinding,
11
    Optional,
12
    Inject,
13
    ChangeDetectorRef
14
} from '@angular/core';
15
import { OverlayRef, Overlay } from '@angular/cdk/overlay';
16
import { InputBoolean, InputNumber, ThyPlacement } from 'ngx-tethys/core';
17
import { ThyAutocompleteService } from './overlay/autocomplete.service';
18
import { ThyAutocompleteRef } from './overlay/autocomplete-ref';
1✔
19
import { ThyAutocompleteComponent } from './autocomplete.component';
20
import { ThyOptionComponent, ThyOptionSelectionChangeEvent } from 'ngx-tethys/shared';
×
21
import { DOCUMENT } from '@angular/common';
×
22
import { Subject, Observable, merge, fromEvent, of, Subscription } from 'rxjs';
23
import { ESCAPE, UP_ARROW, ENTER, DOWN_ARROW, TAB } from 'ngx-tethys/util';
×
24
import { filter, map, take, tap, delay, switchMap } from 'rxjs/operators';
25
import { ScrollToService } from 'ngx-tethys/core';
26
import { warnDeprecation } from 'ngx-tethys/util';
×
27

28
/**
29
 * 自动完成触发指令
×
30
 * @name thyAutocomplete
31
 */
32
@Directive({
×
33
    selector:
×
34
        'input[thyAutocompleteTrigger], textarea[thyAutocompleteTrigger], thy-input[thyAutocompleteTrigger], thy-input-search[thyAutocompleteTrigger], input[thyAutocomplete], textarea[thyAutocomplete], thy-input[thyAutocomplete], thy-input-search[thyAutocomplete]',
35
    exportAs: 'thyAutocompleteTrigger, thyAutocomplete',
×
36
    host: {
37
        '(input)': 'handleInput($event)',
38
        '(focusin)': 'onFocus()',
×
39
        '(keydown)': 'onKeydown($event)'
40
    },
×
41
    standalone: true
42
})
43
export class ThyAutocompleteTriggerDirective implements OnInit, OnDestroy {
×
44
    protected overlayRef: OverlayRef;
×
45

×
46
    private autocompleteRef: ThyAutocompleteRef<ThyAutocompleteComponent>;
×
47

×
48
    private readonly closeKeyEventStream = new Subject<void>();
×
49

×
50
    private closingActionsSubscription: Subscription;
×
51

×
52
    private _autocompleteComponent: ThyAutocompleteComponent;
×
53

×
54
    @HostBinding(`class.thy-autocomplete-opened`) panelOpened = false;
×
55

56
    /**
57
     * 下拉菜单组件实例。已废弃,请使用 thyAutocomplete
58
     * @type thyAutocompleteComponent
×
59
     * @deprecated
×
60
     */
61
    @Input('thyAutocompleteComponent')
62
    set autocompleteComponent(data: ThyAutocompleteComponent) {
63
        if (typeof ngDevMode === 'undefined' || ngDevMode) {
×
64
            warnDeprecation(`The property thyAutocompleteComponent will be deprecated, please use thyAutocomplete instead.`);
65
        }
66
        this._autocompleteComponent = data;
67
    }
68

×
69
    /**
×
70
     * 下拉菜单组件实例
71
     * @type thyAutocompleteComponent
×
72
     */
×
73
    @Input('thyAutocomplete')
×
74
    set autocomplete(data: ThyAutocompleteComponent) {
×
75
        this._autocompleteComponent = data;
76
    }
×
77

×
78
    get autocompleteComponent() {
×
79
        return this._autocompleteComponent;
×
80
    }
×
81

82
    /**
×
83
     * 弹出框默认 offset
×
84
     * @type number
85
     */
×
86
    @Input() @InputNumber() thyOffset = 4;
87

×
88
    /**
89
     * 下拉菜单的宽度,不设置默认与输入框同宽
90
     * @type number
91
     */
92
    @Input() @InputNumber() thyAutocompleteWidth: number;
×
93

×
94
    /**
95
     * 下拉菜单的显示位置,'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom'
96
     * @type string
97
     */
×
98
    @Input() thyPlacement: ThyPlacement = 'bottomLeft';
×
99

100
    /**
×
101
     * 是否允许聚焦时打开下拉菜单
×
102
     * @type boolean
×
103
     */
104
    @Input() @InputBoolean() thyIsFocusOpen = true;
105

×
106
    get activeOption(): ThyOptionComponent | null {
×
107
        if (this.autocompleteComponent && this.autocompleteComponent.keyManager) {
×
108
            return this.autocompleteComponent.keyManager.activeItem;
109
        }
110

×
111
        return null;
×
112
    }
113

114
    get panelClosingActions(): Observable<ThyOptionSelectionChangeEvent | null> {
×
115
        return merge(
×
116
            this.autocompleteComponent.thyOptionSelected,
117
            this.autocompleteComponent.keyManager.tabOut.pipe(filter(() => this.panelOpened)),
118
            this.closeKeyEventStream,
×
119
            this.getOutsideClickStream(),
×
120
            this.overlayRef ? this.overlayRef.detachments().pipe(filter(() => this.panelOpened)) : of()
×
121
        ).pipe(
×
122
            // Normalize the output so we return a consistent type.
123
            map(event => (event instanceof ThyOptionSelectionChangeEvent ? event : null))
124
        );
125
    }
×
126

127
    constructor(
128
        private elementRef: ElementRef,
129
        private ngZone: NgZone,
130
        private overlay: Overlay,
131
        private autocompleteService: ThyAutocompleteService,
×
132
        private viewContainerRef: ViewContainerRef,
133
        @Optional() @Inject(DOCUMENT) private document: any,
×
134
        private cdr: ChangeDetectorRef
×
135
    ) {}
×
136

×
137
    ngOnInit(): void {}
138

139
    onFocus() {
×
140
        if (this.canOpen() && this.thyIsFocusOpen) {
141
            this.openPanel();
142
        }
143
    }
×
144

145
    onKeydown(event: KeyboardEvent) {
×
146
        const keyCode = event.keyCode;
147
        // Prevent the default action on all escape key presses. This is here primarily to bring IE
148
        // in line with other browsers. By default, pressing escape on IE will cause it to revert
149
        // the input value to the one that it had on focus, however it won't dispatch any events
150
        // which means that the model value will be out of sync with the view.
151
        if (keyCode === ESCAPE) {
152
            event.preventDefault();
×
153
        }
×
154
        if (this.activeOption && keyCode === ENTER && this.panelOpened) {
155
            this.activeOption.selectViaInteraction();
156
            this.resetActiveItem();
157
            event.preventDefault();
158
        } else if (this.autocompleteComponent) {
×
159
            const prevActiveItem = this.autocompleteComponent.keyManager.activeItem;
160
            const isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW;
161
            if (this.panelOpened || keyCode === TAB) {
162
                this.autocompleteComponent.keyManager.onKeydown(event);
163
            } else if (isArrowKey && this.canOpen()) {
×
164
                this.openPanel();
×
165
            }
×
166
            if (
167
                (isArrowKey || this.autocompleteComponent.keyManager.activeItem !== prevActiveItem) &&
×
168
                this.autocompleteComponent.keyManager.activeItem
169
            ) {
170
                ScrollToService.scrollToElement(
171
                    this.autocompleteComponent.keyManager.activeItem.element.nativeElement,
172
                    this.autocompleteComponent.optionsContainer.nativeElement
×
173
                );
174
            }
175
        }
×
176
    }
×
177

178
    handleInput(event: KeyboardEvent) {
×
179
        if (this.canOpen() && document.activeElement === event.target) {
180
            this.openPanel();
181
        }
182
    }
×
183

184
    openPanel() {
185
        if (this.overlayRef && this.overlayRef.hasAttached()) {
×
186
            return;
×
187
        }
×
188
        const overlayRef = this.createOverlay();
189
        this.overlayRef = overlayRef;
190
        overlayRef.keydownEvents().subscribe(event => {
191
            // Close when pressing ESCAPE or ALT + UP_ARROW, based on the a11y guidelines.
192
            // See: https://www.w3.org/TR/wai-aria-practices-1.1/#textbox-keyboard-interaction
193
            if (event.keyCode === ESCAPE || (event.keyCode === UP_ARROW && event.altKey)) {
194
                this.resetActiveItem();
195
                this.closeKeyEventStream.next();
196
                // We need to stop propagation, otherwise the event will eventually
×
197
                // reach the input itself and cause the overlay to be reopened.
×
198
                event.stopPropagation();
×
199
                event.preventDefault();
×
200
            }
201
        });
202
        this.panelOpened = true;
×
203
        this.autocompleteComponent.open();
×
204
    }
205

206
    closePanel() {
×
207
        if (this.autocompleteRef) {
208
            this.autocompleteRef.close();
209
            this.cdr.detectChanges();
×
210
            this.closingActionsSubscription.unsubscribe();
×
211
        }
×
212
    }
×
213

214
    createOverlay(): OverlayRef {
215
        const config = Object.assign({
216
            origin: this.elementRef.nativeElement,
×
217
            viewContainerRef: this.viewContainerRef,
×
218
            placement: this.thyPlacement,
219
            offset: this.thyOffset,
1✔
220
            scrollStrategy: this.overlay.scrollStrategies.reposition(),
221
            width: this.thyAutocompleteWidth || this.elementRef.nativeElement.clientWidth
222
        });
223
        this.autocompleteRef = this.autocompleteService.open(this.autocompleteComponent.contentTemplateRef, config);
224
        this.autocompleteRef.afterClosed().subscribe(() => {
225
            this.panelOpened = false;
226
            this.autocompleteComponent.close();
227
        });
228
        // delay 200ms to prevent emit document click rightnow
1✔
229
        this.autocompleteRef
230
            .afterOpened()
231
            .pipe(delay(200))
232
            .subscribe(() => {
233
                this.closingActionsSubscription = this.subscribeToClosingActions();
234
            });
235
        return this.autocompleteRef.getOverlayRef();
236
    }
237

238
    /**
1✔
239
     * This method listens to a stream of panel closing actions and resets the
240
     * stream every time the option list changes.
241
     */
242
    private subscribeToClosingActions(): Subscription {
1✔
243
        const firstStable = this.ngZone.onStable.asObservable().pipe(take(1));
244
        const optionChanges = this.autocompleteComponent.options.changes.pipe(
245
            // Defer emitting to the stream until the next tick, because changing
246
            // bindings in here will cause "changed after checked" errors.
1✔
247
            delay(0)
248
        );
249
        // When the zone is stable initially, and when the option list changes...
250
        return (
1✔
251
            merge(firstStable, optionChanges)
252
                .pipe(
253
                    // create a new stream of panelClosingActions, replacing any previous streams
254
                    // that were created, and flatten it so our stream only emits closing events...
255
                    switchMap(() => {
256
                        this.resetActiveItem();
257

258
                        if (this.panelOpened) {
259
                            this.overlayRef.updatePosition();
260
                        }
261
                        return this.panelClosingActions;
262
                    }),
263
                    // when the first closing event occurs...
264
                    take(1)
265
                )
266
                // set the value, close the panel, and complete.
267
                .subscribe(event => this.setValueAndClose(event))
268
        );
269
    }
270

271
    private setValueAndClose(event: ThyOptionSelectionChangeEvent | null): void {
272
        if (event && event.option) {
273
            this.setValue(event.option.thyLabelText);
274
        }
275
        this.closePanel();
276
    }
277

278
    /** Stream of clicks outside of the autocomplete panel. */
279
    private getOutsideClickStream(): Observable<any> {
280
        return merge(
281
            fromEvent(this.document, 'click') as Observable<MouseEvent>,
282
            fromEvent(this.document, 'touchend') as Observable<TouchEvent>
283
        ).pipe(
284
            filter(event => {
285
                // If we're in the Shadow DOM, the event target will be the shadow root, so we have to
286
                // fall back to check the first element in the path of the click event.
287
                const clickTarget = event.target as HTMLElement;
288
                const formField: any = null;
289

290
                return (
291
                    this.panelOpened &&
292
                    clickTarget !== this.elementRef.nativeElement &&
293
                    !this.elementRef.nativeElement.contains(clickTarget) &&
294
                    (!formField || !formField.contains(clickTarget)) &&
295
                    !!this.overlayRef &&
296
                    !this.overlayRef.overlayElement.contains(clickTarget)
297
                );
298
            })
299
        );
300
    }
301

302
    private setValue(value: string) {
303
        const input = this.elementRef.nativeElement.querySelector('input');
304
        const element = input ? input : this.elementRef.nativeElement;
305
        element.value = value;
306
        element.focus();
307
    }
308

309
    private canOpen(): boolean {
310
        const element: HTMLInputElement = this.elementRef.nativeElement;
311
        return !element.readOnly && !element.disabled;
312
    }
313

314
    private resetActiveItem(): void {
315
        this.autocompleteComponent.keyManager.setActiveItem(this.autocompleteComponent.thyAutoActiveFirstOption ? 0 : -1);
316
    }
317

318
    private destroyPanel(): void {
319
        if (this.overlayRef) {
320
            this.closePanel();
321
            this.overlayRef.dispose();
322
            this.overlayRef = null;
323
        }
324
    }
325

326
    ngOnDestroy() {
327
        this.closeKeyEventStream.complete();
328
        this.destroyPanel();
329
    }
330
}
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