nexheal-lib 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2774,6 +2774,415 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2774
2774
  args: ["window:keydown", ["$event"]]
2775
2775
  }] } });
2776
2776
 
2777
+ class PaginatedAutocompleteControl {
2778
+ subscription = new Subscription();
2779
+ title;
2780
+ required = false;
2781
+ placeholder = "";
2782
+ customClass = '';
2783
+ clearVal = true;
2784
+ error = false;
2785
+ errorMessage = "";
2786
+ autocomplete = "";
2787
+ inputLoader = false;
2788
+ readonly = false;
2789
+ optionDisplayProperty = "displayname";
2790
+ /** 'scroll' = infinite scroll, 'click' = "Load more" button */
2791
+ paginationType = 'scroll';
2792
+ pageSize = 10;
2793
+ searchDebounce = 300;
2794
+ /** Total record count for the current search, from the API response */
2795
+ totalCount = 0;
2796
+ _disabled = false;
2797
+ _pendingPage = 1;
2798
+ /** Items of the page just loaded. Page 1 replaces the list, later pages append. */
2799
+ _displayedOptions = [];
2800
+ set options(value) {
2801
+ const incoming = value || [];
2802
+ if (this._pendingPage <= 1) {
2803
+ this._displayedOptions = [...incoming];
2804
+ }
2805
+ else {
2806
+ // Append, de-duplicating by id in case of overlapping pages
2807
+ const existingIds = new Set(this._displayedOptions.map((o) => o.id));
2808
+ this._displayedOptions = [
2809
+ ...this._displayedOptions,
2810
+ ...incoming.filter((o) => !existingIds.has(o.id)),
2811
+ ];
2812
+ }
2813
+ this.isPageLoading = false;
2814
+ this.processValueBuffer();
2815
+ if (this.isDropdownOpen) {
2816
+ if (this._pendingPage <= 1) {
2817
+ this.highlightedIndex = this._displayedOptions.length > 0 ? 0 : null;
2818
+ }
2819
+ setTimeout(() => {
2820
+ if (this.popperInstance)
2821
+ this.popperInstance.update();
2822
+ }, 0);
2823
+ }
2824
+ }
2825
+ get options() {
2826
+ return this._displayedOptions;
2827
+ }
2828
+ get disabled() {
2829
+ return this._disabled;
2830
+ }
2831
+ set disabled(value) {
2832
+ this._disabled = value;
2833
+ if (this.inputControl) {
2834
+ if (value) {
2835
+ this.inputControl.disable();
2836
+ }
2837
+ else {
2838
+ this.inputControl.enable();
2839
+ }
2840
+ }
2841
+ }
2842
+ pageChange = new EventEmitter();
2843
+ optionSelected = new EventEmitter();
2844
+ selectionCleared = new EventEmitter();
2845
+ blurEvent = new EventEmitter();
2846
+ optionPatched = new EventEmitter();
2847
+ valueBuffer = null;
2848
+ popperInstance;
2849
+ preventDropdownReopen = false;
2850
+ preventClearOnBlur = false;
2851
+ onChange = () => { };
2852
+ onTouched = () => { };
2853
+ inputElement;
2854
+ dropdownElement;
2855
+ inputControl = new FormControl("");
2856
+ isDropdownOpen = false;
2857
+ hasFocus = false;
2858
+ isPageLoading = false;
2859
+ selectedItems;
2860
+ highlightedIndex = null;
2861
+ currentPage = 1;
2862
+ get hasMore() {
2863
+ return this._displayedOptions.length < this.totalCount;
2864
+ }
2865
+ constructor() { }
2866
+ ngOnInit() {
2867
+ this.inputControl.markAsPristine();
2868
+ this.subscription.add(this.inputControl.valueChanges
2869
+ .pipe(debounceTime(this.searchDebounce), distinctUntilChanged())
2870
+ .subscribe((value) => {
2871
+ if (this.preventDropdownReopen) {
2872
+ this.preventDropdownReopen = false;
2873
+ return;
2874
+ }
2875
+ if (this.readonly || !this.hasFocus)
2876
+ return;
2877
+ this.loadPage(1, value || "");
2878
+ }));
2879
+ }
2880
+ ngAfterViewInit() {
2881
+ this.createPopperInstance();
2882
+ }
2883
+ ngOnDestroy() {
2884
+ this.subscription.unsubscribe();
2885
+ if (this.popperInstance) {
2886
+ this.popperInstance.destroy();
2887
+ }
2888
+ }
2889
+ // CVA
2890
+ writeValue(value) {
2891
+ this.preventDropdownReopen = true;
2892
+ if (value == null) {
2893
+ this.valueBuffer = null;
2894
+ this.selectedItems = null;
2895
+ this.inputControl.setValue("", { emitEvent: false });
2896
+ }
2897
+ else {
2898
+ this.valueBuffer = value;
2899
+ this.processValueBuffer();
2900
+ }
2901
+ }
2902
+ processValueBuffer() {
2903
+ if (this.valueBuffer == null) {
2904
+ return;
2905
+ }
2906
+ if (this._displayedOptions.length > 0) {
2907
+ const matchedSuggestion = this._displayedOptions.find((s) => s.id === this.valueBuffer);
2908
+ if (matchedSuggestion) {
2909
+ this.preventDropdownReopen = true;
2910
+ this.inputControl.setValue(matchedSuggestion[this.optionDisplayProperty], { emitEvent: false });
2911
+ this.selectedItems = matchedSuggestion;
2912
+ this.onChange(matchedSuggestion.id);
2913
+ this.optionPatched.emit(matchedSuggestion);
2914
+ this.valueBuffer = null;
2915
+ }
2916
+ }
2917
+ }
2918
+ registerOnChange(fn) {
2919
+ this.onChange = fn;
2920
+ }
2921
+ registerOnTouched(fn) {
2922
+ this.onTouched = fn;
2923
+ }
2924
+ setDisabledState(isDisabled) {
2925
+ if (isDisabled) {
2926
+ this.inputControl.disable();
2927
+ }
2928
+ else {
2929
+ this.inputControl.enable();
2930
+ }
2931
+ }
2932
+ // paging
2933
+ loadPage(page, search) {
2934
+ this.currentPage = page;
2935
+ this._pendingPage = page;
2936
+ this.isPageLoading = true;
2937
+ if (page === 1) {
2938
+ this.openDropdown();
2939
+ }
2940
+ this.pageChange.emit({
2941
+ search: search,
2942
+ page: page,
2943
+ pageSize: this.pageSize,
2944
+ });
2945
+ }
2946
+ loadNextPage() {
2947
+ if (!this.hasMore || this.isPageLoading)
2948
+ return;
2949
+ this.loadPage(this.currentPage + 1, this.inputControl.value || "");
2950
+ }
2951
+ onListScroll(event) {
2952
+ if (this.paginationType !== 'scroll')
2953
+ return;
2954
+ const el = event.target;
2955
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40) {
2956
+ this.loadNextPage();
2957
+ }
2958
+ }
2959
+ onLoadMoreClick(event) {
2960
+ event.stopPropagation();
2961
+ this.loadNextPage();
2962
+ }
2963
+ openDropdown() {
2964
+ if (this.isDropdownOpen)
2965
+ return;
2966
+ this.isDropdownOpen = true;
2967
+ this.highlightedIndex = null;
2968
+ setTimeout(() => {
2969
+ this.createPopperInstance();
2970
+ }, 0);
2971
+ }
2972
+ // selection
2973
+ selectSuggestion(suggestion) {
2974
+ this.preventDropdownReopen = true;
2975
+ this.inputControl.setValue(suggestion[this.optionDisplayProperty], {
2976
+ emitEvent: false,
2977
+ });
2978
+ this.onChange(suggestion.id);
2979
+ this.inputControl.markAsTouched();
2980
+ this.inputControl.updateValueAndValidity();
2981
+ this.isDropdownOpen = false;
2982
+ if (this.selectedItems?.id !== suggestion.id) {
2983
+ this.selectedItems = suggestion;
2984
+ this.optionSelected.emit(suggestion);
2985
+ }
2986
+ }
2987
+ // events
2988
+ onFocus() {
2989
+ this.hasFocus = true;
2990
+ if (this.readonly)
2991
+ return;
2992
+ this.loadPage(1, this.inputControl.value || "");
2993
+ }
2994
+ onInputClick() {
2995
+ if (this.readonly || this.isDropdownOpen)
2996
+ return;
2997
+ if (this.hasFocus) {
2998
+ this.loadPage(1, this.inputControl.value || "");
2999
+ }
3000
+ }
3001
+ onBlur() {
3002
+ this.blurEvent.emit();
3003
+ this.onTouched();
3004
+ // Delay so option / load-more mousedown and click can occur first.
3005
+ setTimeout(() => {
3006
+ if (this.preventClearOnBlur) {
3007
+ this.preventClearOnBlur = false;
3008
+ // Keep the dropdown alive for load-more clicks
3009
+ if (this.inputElement) {
3010
+ this.inputElement.nativeElement.focus();
3011
+ }
3012
+ return;
3013
+ }
3014
+ this.hasFocus = false;
3015
+ this.isDropdownOpen = false;
3016
+ this.isPageLoading = false;
3017
+ const currentValue = this.inputControl.value;
3018
+ const found = this._displayedOptions.find((opt) => String(opt[this.optionDisplayProperty]).toLowerCase() ===
3019
+ String(currentValue).toLowerCase());
3020
+ if (!found) {
3021
+ this.inputControl.setValue("", { emitEvent: false });
3022
+ this.selectedItems = null;
3023
+ this.onChange(null);
3024
+ }
3025
+ }, 300);
3026
+ }
3027
+ onKeyDown(event) {
3028
+ if (!this.isDropdownOpen) {
3029
+ return;
3030
+ }
3031
+ switch (event.key) {
3032
+ case "ArrowDown":
3033
+ this.highlightedIndex =
3034
+ this.highlightedIndex === null ||
3035
+ this.highlightedIndex === this._displayedOptions.length - 1
3036
+ ? 0
3037
+ : this.highlightedIndex + 1;
3038
+ event.preventDefault();
3039
+ this.scrollHighlightedItemIntoView();
3040
+ break;
3041
+ case "ArrowUp":
3042
+ this.highlightedIndex =
3043
+ this.highlightedIndex === null || this.highlightedIndex === 0
3044
+ ? this._displayedOptions.length - 1
3045
+ : this.highlightedIndex - 1;
3046
+ event.preventDefault();
3047
+ this.scrollHighlightedItemIntoView();
3048
+ break;
3049
+ case "Enter":
3050
+ if (this.highlightedIndex !== null &&
3051
+ this._displayedOptions.length > 0) {
3052
+ this.selectSuggestion(this._displayedOptions[this.highlightedIndex]);
3053
+ }
3054
+ else {
3055
+ this.isDropdownOpen = false;
3056
+ }
3057
+ event.preventDefault();
3058
+ break;
3059
+ case "Escape":
3060
+ this.isDropdownOpen = false;
3061
+ break;
3062
+ default:
3063
+ break;
3064
+ }
3065
+ }
3066
+ onOptionMouseDown() {
3067
+ this.preventClearOnBlur = true;
3068
+ }
3069
+ onMouseOver(index) {
3070
+ this.highlightedIndex = index;
3071
+ }
3072
+ // popper
3073
+ createPopperInstance() {
3074
+ if (this.popperInstance) {
3075
+ this.popperInstance.destroy();
3076
+ }
3077
+ if (this.inputElement && this.dropdownElement) {
3078
+ this.popperInstance = createPopper(this.inputElement.nativeElement, this.dropdownElement.nativeElement, {
3079
+ placement: "bottom-start",
3080
+ modifiers: [
3081
+ {
3082
+ name: "offset",
3083
+ options: {
3084
+ offset: [0, 1],
3085
+ },
3086
+ },
3087
+ {
3088
+ name: "flip",
3089
+ options: {
3090
+ fallbackPlacements: ["top-start", "bottom-start"],
3091
+ },
3092
+ },
3093
+ ],
3094
+ });
3095
+ }
3096
+ }
3097
+ scrollHighlightedItemIntoView() {
3098
+ if (this.highlightedIndex !== null) {
3099
+ const highlightedItem = this.dropdownElement.nativeElement.querySelectorAll(".list-item")[this.highlightedIndex];
3100
+ if (highlightedItem) {
3101
+ highlightedItem.scrollIntoView({ block: "nearest" });
3102
+ }
3103
+ }
3104
+ }
3105
+ // clear
3106
+ resetInput() {
3107
+ this.inputControl.setValue("", { emitEvent: false });
3108
+ this.selectedItems = null;
3109
+ this.valueBuffer = null;
3110
+ this.onChange(null);
3111
+ this.selectionCleared.emit();
3112
+ if (this.hasFocus) {
3113
+ this.loadPage(1, "");
3114
+ }
3115
+ }
3116
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: PaginatedAutocompleteControl, deps: [], target: i0.ɵɵFactoryTarget.Component });
3117
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: PaginatedAutocompleteControl, isStandalone: true, selector: "paginated-autocomplete-control", inputs: { title: "title", required: "required", placeholder: "placeholder", customClass: "customClass", clearVal: "clearVal", error: "error", errorMessage: "errorMessage", autocomplete: "autocomplete", inputLoader: "inputLoader", readonly: "readonly", optionDisplayProperty: "optionDisplayProperty", paginationType: "paginationType", pageSize: "pageSize", searchDebounce: "searchDebounce", totalCount: "totalCount", options: "options", disabled: "disabled" }, outputs: { pageChange: "pageChange", optionSelected: "optionSelected", selectionCleared: "selectionCleared", blurEvent: "blurEvent", optionPatched: "optionPatched" }, providers: [
3118
+ {
3119
+ provide: NG_VALUE_ACCESSOR,
3120
+ useExisting: PaginatedAutocompleteControl,
3121
+ multi: true,
3122
+ },
3123
+ ], viewQueries: [{ propertyName: "inputElement", first: true, predicate: ["inputElement"], descendants: true }, { propertyName: "dropdownElement", first: true, predicate: ["dropdownElement"], descendants: true }], ngImport: i0, template: "<div class=\"form-group auto-complete paginated-autocomplete\" [ngClass]=\"customClass\" [class.readonly]=\"readonly\">\n @if (title) {\n <label class=\"inp-label\" [ngClass]=\"{ 'required': required }\">{{ title }}</label>\n }\n\n <input #inputElement type=\"text\" class=\"form-control\" [placeholder]=\"placeholder\" [formControl]=\"inputControl\"\n (blur)=\"onBlur()\" (keydown)=\"onKeyDown($event)\" (focus)=\"onFocus()\" (click)=\"onInputClick()\"\n [ngClass]=\"{'is-invalid': error}\" [attr.autocomplete]=\"autocomplete || null\" [readonly]=\"readonly\" />\n\n <span class=\"focus-border\"></span>\n\n @if (!inputLoader && inputControl.value && clearVal && hasFocus) {\n <label class=\"clear\" (mousedown)=\"onOptionMouseDown()\" (click)=\"resetInput()\">\n <i class=\"he he-close\"></i>\n </label>\n }\n\n @if (isDropdownOpen) {\n <div #dropdownElement class=\"option-list\" (scroll)=\"onListScroll($event)\">\n @if (options.length === 0 && !isPageLoading) {\n <div class=\"no-results\">\n <div>No results found</div>\n </div>\n } @else {\n @for (suggestion of options; track suggestion.id; let i = $index) {\n <div class=\"list-item\" [ngClass]=\"{\n 'active': suggestion === selectedItems,\n 'highlighted': highlightedIndex === i\n }\" (mousedown)=\"onOptionMouseDown()\" (click)=\"selectSuggestion(suggestion)\" (mouseover)=\"onMouseOver(i)\">\n {{ suggestion[optionDisplayProperty] }}\n </div>\n }\n\n @if (isPageLoading) {\n <div class=\"list-loading\">Loading...</div>\n }\n\n @if (paginationType === 'click' && hasMore && !isPageLoading) {\n <div class=\"load-more\" (mousedown)=\"onOptionMouseDown()\" (click)=\"onLoadMoreClick($event)\">\n Load more ({{ options.length }} of {{ totalCount }})\n </div>\n }\n }\n </div>\n }\n\n @if (inputLoader) {\n <label class=\"loader input-loader\"></label>\n }\n\n @if (error) {\n <div class=\"val-msg\">{{ errorMessage }}</div>\n }\n</div>\n", styles: [".form-group.paginated-autocomplete .form-control{padding-right:unset}.form-group.paginated-autocomplete .clear{right:7px}.form-group.paginated-autocomplete .option-list{max-height:220px;overflow-y:auto}.form-group.paginated-autocomplete .option-list .no-results{padding:10px;color:#9b9b9b;text-align:center}.form-group.paginated-autocomplete .option-list .list-loading{padding:8px 10px;color:#9b9b9b;text-align:center;font-size:12px}.form-group.paginated-autocomplete .option-list .load-more{padding:8px 10px;text-align:center;cursor:pointer;color:#0d6efd;font-size:13px;border-top:1px solid #eee;-webkit-user-select:none;user-select:none}.form-group.paginated-autocomplete .option-list .load-more:hover{background-color:#f5f5f5}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
3124
+ }
3125
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: PaginatedAutocompleteControl, decorators: [{
3126
+ type: Component,
3127
+ args: [{ selector: "paginated-autocomplete-control", standalone: true, imports: [CommonModule, ReactiveFormsModule], providers: [
3128
+ {
3129
+ provide: NG_VALUE_ACCESSOR,
3130
+ useExisting: PaginatedAutocompleteControl,
3131
+ multi: true,
3132
+ },
3133
+ ], template: "<div class=\"form-group auto-complete paginated-autocomplete\" [ngClass]=\"customClass\" [class.readonly]=\"readonly\">\n @if (title) {\n <label class=\"inp-label\" [ngClass]=\"{ 'required': required }\">{{ title }}</label>\n }\n\n <input #inputElement type=\"text\" class=\"form-control\" [placeholder]=\"placeholder\" [formControl]=\"inputControl\"\n (blur)=\"onBlur()\" (keydown)=\"onKeyDown($event)\" (focus)=\"onFocus()\" (click)=\"onInputClick()\"\n [ngClass]=\"{'is-invalid': error}\" [attr.autocomplete]=\"autocomplete || null\" [readonly]=\"readonly\" />\n\n <span class=\"focus-border\"></span>\n\n @if (!inputLoader && inputControl.value && clearVal && hasFocus) {\n <label class=\"clear\" (mousedown)=\"onOptionMouseDown()\" (click)=\"resetInput()\">\n <i class=\"he he-close\"></i>\n </label>\n }\n\n @if (isDropdownOpen) {\n <div #dropdownElement class=\"option-list\" (scroll)=\"onListScroll($event)\">\n @if (options.length === 0 && !isPageLoading) {\n <div class=\"no-results\">\n <div>No results found</div>\n </div>\n } @else {\n @for (suggestion of options; track suggestion.id; let i = $index) {\n <div class=\"list-item\" [ngClass]=\"{\n 'active': suggestion === selectedItems,\n 'highlighted': highlightedIndex === i\n }\" (mousedown)=\"onOptionMouseDown()\" (click)=\"selectSuggestion(suggestion)\" (mouseover)=\"onMouseOver(i)\">\n {{ suggestion[optionDisplayProperty] }}\n </div>\n }\n\n @if (isPageLoading) {\n <div class=\"list-loading\">Loading...</div>\n }\n\n @if (paginationType === 'click' && hasMore && !isPageLoading) {\n <div class=\"load-more\" (mousedown)=\"onOptionMouseDown()\" (click)=\"onLoadMoreClick($event)\">\n Load more ({{ options.length }} of {{ totalCount }})\n </div>\n }\n }\n </div>\n }\n\n @if (inputLoader) {\n <label class=\"loader input-loader\"></label>\n }\n\n @if (error) {\n <div class=\"val-msg\">{{ errorMessage }}</div>\n }\n</div>\n", styles: [".form-group.paginated-autocomplete .form-control{padding-right:unset}.form-group.paginated-autocomplete .clear{right:7px}.form-group.paginated-autocomplete .option-list{max-height:220px;overflow-y:auto}.form-group.paginated-autocomplete .option-list .no-results{padding:10px;color:#9b9b9b;text-align:center}.form-group.paginated-autocomplete .option-list .list-loading{padding:8px 10px;color:#9b9b9b;text-align:center;font-size:12px}.form-group.paginated-autocomplete .option-list .load-more{padding:8px 10px;text-align:center;cursor:pointer;color:#0d6efd;font-size:13px;border-top:1px solid #eee;-webkit-user-select:none;user-select:none}.form-group.paginated-autocomplete .option-list .load-more:hover{background-color:#f5f5f5}\n"] }]
3134
+ }], ctorParameters: () => [], propDecorators: { title: [{
3135
+ type: Input
3136
+ }], required: [{
3137
+ type: Input
3138
+ }], placeholder: [{
3139
+ type: Input
3140
+ }], customClass: [{
3141
+ type: Input
3142
+ }], clearVal: [{
3143
+ type: Input
3144
+ }], error: [{
3145
+ type: Input
3146
+ }], errorMessage: [{
3147
+ type: Input
3148
+ }], autocomplete: [{
3149
+ type: Input
3150
+ }], inputLoader: [{
3151
+ type: Input
3152
+ }], readonly: [{
3153
+ type: Input
3154
+ }], optionDisplayProperty: [{
3155
+ type: Input
3156
+ }], paginationType: [{
3157
+ type: Input
3158
+ }], pageSize: [{
3159
+ type: Input
3160
+ }], searchDebounce: [{
3161
+ type: Input
3162
+ }], totalCount: [{
3163
+ type: Input
3164
+ }], options: [{
3165
+ type: Input
3166
+ }], disabled: [{
3167
+ type: Input
3168
+ }], pageChange: [{
3169
+ type: Output
3170
+ }], optionSelected: [{
3171
+ type: Output
3172
+ }], selectionCleared: [{
3173
+ type: Output
3174
+ }], blurEvent: [{
3175
+ type: Output
3176
+ }], optionPatched: [{
3177
+ type: Output
3178
+ }], inputElement: [{
3179
+ type: ViewChild,
3180
+ args: ["inputElement"]
3181
+ }], dropdownElement: [{
3182
+ type: ViewChild,
3183
+ args: ["dropdownElement"]
3184
+ }] } });
3185
+
2777
3186
  /**
2778
3187
  * A custom radio-button group.
2779
3188
  *
@@ -3750,5 +4159,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3750
4159
  * Generated bundle index. Do not edit.
3751
4160
  */
3752
4161
 
3753
- export { AutocompleteControl, CalendarControl, CheckboxControl, ColorPicker, InputControl, MultiselectControl, RadioControl, RichTextEditor, SelectControl, SwitchControl, TextareaControl };
4162
+ export { AutocompleteControl, CalendarControl, CheckboxControl, ColorPicker, InputControl, MultiselectControl, PaginatedAutocompleteControl, RadioControl, RichTextEditor, SelectControl, SwitchControl, TextareaControl };
3754
4163
  //# sourceMappingURL=nexheal-lib.mjs.map