@colijnit/corecomponents_v12 261.20.17 → 261.20.18

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.
@@ -1,6 +1,6 @@
1
1
  import { __decorate } from 'tslib';
2
2
  import * as i0 from '@angular/core';
3
- import { Input, Injectable, HostBinding, ViewEncapsulation, Component, Directive, ElementRef, Pipe, EventEmitter, Output, ViewChildren, ViewContainerRef, HostListener, ViewChild, Optional, NgModule, SkipSelf, InjectionToken, Inject, forwardRef, ChangeDetectionStrategy, ContentChildren, NO_ERRORS_SCHEMA, Injector, QueryList } from '@angular/core';
3
+ import { Input, Injectable, HostBinding, ViewEncapsulation, Component, Directive, ElementRef, Pipe, EventEmitter, Output, ViewChildren, ViewContainerRef, HostListener, ViewChild, Optional, NgModule, SkipSelf, InjectionToken, Inject, forwardRef, ChangeDetectionStrategy, ContentChildren, LOCALE_ID, NO_ERRORS_SCHEMA, Injector, QueryList } from '@angular/core';
4
4
  import * as i5 from '@angular/forms';
5
5
  import { NgModel, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
6
6
  import * as i1 from '@angular/platform-browser';
@@ -532,6 +532,28 @@ class NumberUtils {
532
532
  return { isOk: false, nearestOkNr: nearestOkNr };
533
533
  }
534
534
  }
535
+ /**
536
+ * Decides whether typing one more digit onto the given numeric text would exceed the configured
537
+ * digit limits. When `decimals` (scale) is set, `maxLength` is treated as the total precision: the
538
+ * integer part is capped at (maxLength - decimals) digits and the fractional part at `decimals`
539
+ * digits. When `decimals` is not set, `maxLength` caps the total number of digits.
540
+ *
541
+ * Note: assumes the digit is appended at the end of the value, because native number inputs expose
542
+ * no caret/selection information to constrain mid-string edits.
543
+ */
544
+ static WouldExceedDigitLimits(currentValue, maxLength, decimals) {
545
+ const digits = ('' + (currentValue ?? '')).replace('-', '');
546
+ if (decimals > 0) {
547
+ const separatorIndex = digits.search(/[.,]/);
548
+ if (separatorIndex === -1) {
549
+ const maxIntegerDigits = maxLength > 0 ? maxLength - decimals : Number.POSITIVE_INFINITY;
550
+ return digits.length >= maxIntegerDigits;
551
+ }
552
+ const decimalDigits = digits.length - separatorIndex - 1;
553
+ return decimalDigits >= decimals;
554
+ }
555
+ return maxLength > 0 && digits.length >= maxLength;
556
+ }
535
557
  static _GetLargestDecimalPrecisionOf(...args) {
536
558
  return ArrayUtils.GetMaxCalculatedValue(args, (arg) => {
537
559
  return NumberUtils.GetDecimalPlaces(arg);
@@ -934,8 +956,12 @@ function requiredValidator(control) {
934
956
  function precisionScaleValidator(precision, scale) {
935
957
  return function (control) {
936
958
  let isValid = true;
937
- if (control) {
938
- isValid = NumberUtils.CheckPrecisionAndScale(control.value, precision, scale).isOk;
959
+ if (control && control.value !== null && control.value !== undefined && control.value !== '') {
960
+ // The value may be a locale display string (e.g. "1234567,89"); normalize it to a number so the
961
+ // decimal separator is not counted as an integer digit by CheckPrecisionAndScale.
962
+ const raw = control.value;
963
+ const numeric = typeof raw === 'string' ? Number(raw.replace(',', '.')) : raw;
964
+ isValid = isNaN(numeric) || NumberUtils.CheckPrecisionAndScale(numeric, precision, scale).isOk;
939
965
  }
940
966
  return isValid ? null : { 'precision-scale': "MESSAGE_FIELD_PRECISION_OVERRIDDEN||" + precision };
941
967
  };
@@ -2031,27 +2057,28 @@ class CommitButtonsComponent {
2031
2057
  event.currentTarget.removeEventListener('animationiteration', this._handleAnimationIteration);
2032
2058
  };
2033
2059
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CommitButtonsComponent, deps: [{ token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
2034
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: CommitButtonsComponent, isStandalone: false, selector: "co-commit-buttons", inputs: { committing: "committing", commitFinished: "commitFinished", parentForOverlay: "parentForOverlay" }, outputs: { cancelClick: "cancelClick", commitClick: "commitClick" }, host: { properties: { "class.co-commit-buttons": "this.showClass" } }, viewQueries: [{ propertyName: "content", predicate: ["animatediv"], descendants: true, read: ElementRef }], ngImport: i0, template: `
2035
- <div class="commit-buttons-wrapper" [overlay]="parentForOverlay" [rightAlign]="true" @showHideSaveCancel>
2036
- <div class="commit-buttons-button save" [class.finished]="commitFinished"
2037
- (click)="commitClick.emit($event)">
2038
- @if (committing) {
2039
- <div class="save-button-spinner">
2040
- <div #animatediv></div>
2041
- <div #animatediv></div>
2042
- <div #animatediv></div>
2043
- <div #animatediv></div>
2044
- </div>
2045
- }
2046
- @if (firstShow || commitFinished) {
2047
- <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
2048
- <path class="checkmark-check" [class.first-show]="firstShow" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg>
2049
- }
2050
- </div>
2051
- <div class="commit-buttons-button cancel" (mousedown)="$event.preventDefault()" (click)="cancelClick.emit($event)">
2052
- <div class="cancel-button"></div>
2053
- </div>
2054
- </div>
2060
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: CommitButtonsComponent, isStandalone: false, selector: "co-commit-buttons", inputs: { committing: "committing", commitFinished: "commitFinished", parentForOverlay: "parentForOverlay" }, outputs: { cancelClick: "cancelClick", commitClick: "commitClick" }, host: { properties: { "class.co-commit-buttons": "this.showClass" } }, viewQueries: [{ propertyName: "content", predicate: ["animatediv"], descendants: true, read: ElementRef }], ngImport: i0, template: `
2061
+ <div class="commit-buttons-wrapper" [overlay]="parentForOverlay" [rightAlign]="true" @showHideSaveCancel>
2062
+ <div class="commit-buttons-button save" [class.finished]="commitFinished"
2063
+ (mousedown)="$event.preventDefault()"
2064
+ (click)="commitClick.emit($event)">
2065
+ @if (committing) {
2066
+ <div class="save-button-spinner">
2067
+ <div #animatediv></div>
2068
+ <div #animatediv></div>
2069
+ <div #animatediv></div>
2070
+ <div #animatediv></div>
2071
+ </div>
2072
+ }
2073
+ @if (firstShow || commitFinished) {
2074
+ <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
2075
+ <path class="checkmark-check" [class.first-show]="firstShow" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg>
2076
+ }
2077
+ </div>
2078
+ <div class="commit-buttons-button cancel" (mousedown)="$event.preventDefault()" (click)="cancelClick.emit($event)">
2079
+ <div class="cancel-button"></div>
2080
+ </div>
2081
+ </div>
2055
2082
  `, isInline: true, dependencies: [{ kind: "directive", type: OverlayDirective, selector: "[overlay]", inputs: ["overlay", "view", "inline", "keepInView", "inheritWidth", "rightAlign", "fullSize"] }], animations: [
2056
2083
  trigger('showHideSaveCancel', [
2057
2084
  state('*', style({ transform: 'scaleY(1)', opacity: 1 })),
@@ -2064,27 +2091,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
2064
2091
  type: Component,
2065
2092
  args: [{
2066
2093
  selector: 'co-commit-buttons',
2067
- template: `
2068
- <div class="commit-buttons-wrapper" [overlay]="parentForOverlay" [rightAlign]="true" @showHideSaveCancel>
2069
- <div class="commit-buttons-button save" [class.finished]="commitFinished"
2070
- (click)="commitClick.emit($event)">
2071
- @if (committing) {
2072
- <div class="save-button-spinner">
2073
- <div #animatediv></div>
2074
- <div #animatediv></div>
2075
- <div #animatediv></div>
2076
- <div #animatediv></div>
2077
- </div>
2078
- }
2079
- @if (firstShow || commitFinished) {
2080
- <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
2081
- <path class="checkmark-check" [class.first-show]="firstShow" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg>
2082
- }
2083
- </div>
2084
- <div class="commit-buttons-button cancel" (mousedown)="$event.preventDefault()" (click)="cancelClick.emit($event)">
2085
- <div class="cancel-button"></div>
2086
- </div>
2087
- </div>
2094
+ template: `
2095
+ <div class="commit-buttons-wrapper" [overlay]="parentForOverlay" [rightAlign]="true" @showHideSaveCancel>
2096
+ <div class="commit-buttons-button save" [class.finished]="commitFinished"
2097
+ (mousedown)="$event.preventDefault()"
2098
+ (click)="commitClick.emit($event)">
2099
+ @if (committing) {
2100
+ <div class="save-button-spinner">
2101
+ <div #animatediv></div>
2102
+ <div #animatediv></div>
2103
+ <div #animatediv></div>
2104
+ <div #animatediv></div>
2105
+ </div>
2106
+ }
2107
+ @if (firstShow || commitFinished) {
2108
+ <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
2109
+ <path class="checkmark-check" [class.first-show]="firstShow" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg>
2110
+ }
2111
+ </div>
2112
+ <div class="commit-buttons-button cancel" (mousedown)="$event.preventDefault()" (click)="cancelClick.emit($event)">
2113
+ <div class="cancel-button"></div>
2114
+ </div>
2115
+ </div>
2088
2116
  `,
2089
2117
  animations: [
2090
2118
  trigger('showHideSaveCancel', [
@@ -2267,7 +2295,7 @@ class BaseInputComponent {
2267
2295
  get model() {
2268
2296
  return this._model;
2269
2297
  }
2270
- label;
2298
+ label = '';
2271
2299
  noValidation = false;
2272
2300
  initFocus;
2273
2301
  noClickFocus;
@@ -2517,7 +2545,6 @@ class BaseInputComponent {
2517
2545
  // @implements ConfigurationAdapterComponent
2518
2546
  objectConfigName;
2519
2547
  validationError = '';
2520
- shouldCommit = true;
2521
2548
  _markedAsUserTouched = false;
2522
2549
  _destroyed = false;
2523
2550
  _hasOnPushCdStrategy = false;
@@ -2543,6 +2570,7 @@ class BaseInputComponent {
2543
2570
  _formReadonlyChangeSub;
2544
2571
  _clearMarkedAsUserTouchedSub;
2545
2572
  _canSaveOrCancel = false;
2573
+ _committing = false;
2546
2574
  _commitButtonsComponentRef;
2547
2575
  _validationComponentRef;
2548
2576
  constructor(changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef) {
@@ -2614,7 +2642,6 @@ class BaseInputComponent {
2614
2642
  return Promise.resolve(true);
2615
2643
  };
2616
2644
  cancelClick(event) {
2617
- this.shouldCommit = false;
2618
2645
  this.keepFocus = true;
2619
2646
  if (this._initialModelSet) {
2620
2647
  this.model = this._initialModel;
@@ -2669,7 +2696,7 @@ class BaseInputComponent {
2669
2696
  this.focus.next();
2670
2697
  }
2671
2698
  async doBlur(event, handleCommit = true) {
2672
- if (this.showSaveCancel && handleCommit && this._modelDirtyForCommit && this.shouldCommit) {
2699
+ if (this.showSaveCancel && handleCommit && this._modelDirtyForCommit) {
2673
2700
  await this._handleCommit(event, false);
2674
2701
  }
2675
2702
  setTimeout(() => {
@@ -2799,23 +2826,37 @@ class BaseInputComponent {
2799
2826
  }
2800
2827
  }
2801
2828
  async _handleCommit(event, doBlur = true) {
2802
- if (!this.showSaveCancel || (!this._modelDirtyForCommit)) {
2829
+ if (!this.showSaveCancel || !this._modelDirtyForCommit || this._committing) {
2803
2830
  return true;
2804
2831
  }
2832
+ this._committing = true;
2805
2833
  this.keepFocus = true;
2806
- this.shouldCommit = true;
2807
2834
  if (this._commitButtonsComponentRef) {
2808
2835
  this._commitButtonsComponentRef.instance.commitFinished = false;
2809
2836
  this._commitButtonsComponentRef.instance.committing = true;
2810
2837
  }
2811
2838
  this._modelDirtyForCommit = false;
2812
- const success = await this.commit(this.model);
2813
- this.keepFocus = false;
2814
- await this._commitFinished();
2815
- if (success && doBlur) {
2816
- this.doBlur(event, false);
2839
+ try {
2840
+ const success = await this.commit(this.model);
2841
+ if (success) {
2842
+ // keep the committed value as the new baseline, otherwise a later model re-emission
2843
+ // would compare against a stale _initialModel and re-open the commit guard.
2844
+ this._initialModel = this._model;
2845
+ this._modelDirtyForCommit = false;
2846
+ }
2847
+ else {
2848
+ this._modelDirtyForCommit = true;
2849
+ }
2850
+ this.keepFocus = false;
2851
+ await this._commitFinished();
2852
+ if (success && doBlur) {
2853
+ this.doBlur(event, false);
2854
+ }
2855
+ return success;
2856
+ }
2857
+ finally {
2858
+ this._committing = false;
2817
2859
  }
2818
- return success;
2819
2860
  }
2820
2861
  _commitFinished() {
2821
2862
  return new Promise((resolve) => {
@@ -2848,6 +2889,8 @@ class BaseInputComponent {
2848
2889
  this.input.disabled = disabled;
2849
2890
  }
2850
2891
  }
2892
+ // protected so numeric components can suppress the native maxlength (which, on a text input, would
2893
+ // wrongly count the decimal separator/sign as characters); they enforce precision/scale in keydown.
2851
2894
  _setNativeMaxLength(maxlength) {
2852
2895
  if (this.input && !isNaN(maxlength)) {
2853
2896
  this.input.maxLength = maxlength;
@@ -5802,48 +5845,40 @@ class GridToolbarComponent {
5802
5845
  }
5803
5846
  }
5804
5847
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: GridToolbarComponent, deps: [{ token: IconCacheService }], target: i0.ɵɵFactoryTarget.Component });
5805
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: GridToolbarComponent, isStandalone: false, selector: "co-grid-toolbar", inputs: { showEdit: "showEdit", showAdd: "showAdd", showDelete: "showDelete", deleteEnabled: "deleteEnabled" }, outputs: { editClick: "editClick", cancelClick: "cancelClick", saveClick: "saveClick", addClick: "addClick", deleteClick: "deleteClick" }, host: { properties: { "class.co-grid-toolbar": "this.showClass" } }, ngImport: i0, template: `
5806
- <div class="grid-toolbar-wrapper">
5807
- @if (showEdit) {
5808
- <co-icon [iconData]="iconsService.getIcon(icons.PenToSquareSolid)" [title]="'edit'" (click)="editClick.emit($event)"></co-icon>
5809
- }
5810
- @if (showEdit) {
5811
- <co-icon [iconData]="iconsService.getIcon(icons.RotateLeftSolid)" [title]="'cancel'" (click)="cancelClick.emit()"></co-icon>
5812
- }
5813
- @if (showEdit) {
5814
- <co-icon [iconData]="iconsService.getIcon(icons.FloppyDiskSolid)" [title]="'save'" (click)="saveClick.emit()"></co-icon>
5815
- }
5816
- @if (showAdd || showEdit) {
5817
- <co-icon [iconData]="iconsService.getIcon(icons.PlusSolid)" [title]="'add'" (click)="addClick.emit()"></co-icon>
5818
- }
5819
- @if (showDelete) {
5820
- <co-icon [iconData]="iconsService.getIcon(icons.TrashCanSolid)" [title]="'delete'" [class.disabled]="!deleteEnabled" (click)="handleDeleteClick()"></co-icon>
5821
- }
5822
- </div>
5848
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: GridToolbarComponent, isStandalone: false, selector: "co-grid-toolbar", inputs: { showEdit: "showEdit", showAdd: "showAdd", showDelete: "showDelete", deleteEnabled: "deleteEnabled" }, outputs: { editClick: "editClick", cancelClick: "cancelClick", saveClick: "saveClick", addClick: "addClick", deleteClick: "deleteClick" }, host: { properties: { "class.co-grid-toolbar": "this.showClass" } }, ngImport: i0, template: `
5849
+ <div class="grid-toolbar-wrapper">
5850
+ @if (showEdit) {
5851
+ <co-icon [iconData]="iconsService.getIcon(icons.PenToSquareSolid)" [title]="'edit'" (click)="editClick.emit($event)"></co-icon>
5852
+ <co-icon [iconData]="iconsService.getIcon(icons.RotateLeftSolid)" [title]="'cancel'" (click)="cancelClick.emit()"></co-icon>
5853
+ <co-icon [iconData]="iconsService.getIcon(icons.FloppyDiskSolid)" [title]="'save'" (click)="saveClick.emit()"></co-icon>
5854
+ }
5855
+ @if (showAdd || showEdit) {
5856
+ <co-icon [iconData]="iconsService.getIcon(icons.PlusSolid)" [title]="'add'" (click)="addClick.emit()"></co-icon>
5857
+ }
5858
+ @if (showDelete) {
5859
+ <co-icon [iconData]="iconsService.getIcon(icons.TrashCanSolid)" [title]="'delete'" [class.disabled]="!deleteEnabled" (click)="handleDeleteClick()"></co-icon>
5860
+ }
5861
+ </div>
5823
5862
  `, isInline: true, dependencies: [{ kind: "component", type: IconComponent, selector: "co-icon", inputs: ["icon", "iconData"] }], encapsulation: i0.ViewEncapsulation.None });
5824
5863
  }
5825
5864
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: GridToolbarComponent, decorators: [{
5826
5865
  type: Component,
5827
5866
  args: [{
5828
5867
  selector: "co-grid-toolbar",
5829
- template: `
5830
- <div class="grid-toolbar-wrapper">
5831
- @if (showEdit) {
5832
- <co-icon [iconData]="iconsService.getIcon(icons.PenToSquareSolid)" [title]="'edit'" (click)="editClick.emit($event)"></co-icon>
5833
- }
5834
- @if (showEdit) {
5835
- <co-icon [iconData]="iconsService.getIcon(icons.RotateLeftSolid)" [title]="'cancel'" (click)="cancelClick.emit()"></co-icon>
5836
- }
5837
- @if (showEdit) {
5838
- <co-icon [iconData]="iconsService.getIcon(icons.FloppyDiskSolid)" [title]="'save'" (click)="saveClick.emit()"></co-icon>
5839
- }
5840
- @if (showAdd || showEdit) {
5841
- <co-icon [iconData]="iconsService.getIcon(icons.PlusSolid)" [title]="'add'" (click)="addClick.emit()"></co-icon>
5842
- }
5843
- @if (showDelete) {
5844
- <co-icon [iconData]="iconsService.getIcon(icons.TrashCanSolid)" [title]="'delete'" [class.disabled]="!deleteEnabled" (click)="handleDeleteClick()"></co-icon>
5845
- }
5846
- </div>
5868
+ template: `
5869
+ <div class="grid-toolbar-wrapper">
5870
+ @if (showEdit) {
5871
+ <co-icon [iconData]="iconsService.getIcon(icons.PenToSquareSolid)" [title]="'edit'" (click)="editClick.emit($event)"></co-icon>
5872
+ <co-icon [iconData]="iconsService.getIcon(icons.RotateLeftSolid)" [title]="'cancel'" (click)="cancelClick.emit()"></co-icon>
5873
+ <co-icon [iconData]="iconsService.getIcon(icons.FloppyDiskSolid)" [title]="'save'" (click)="saveClick.emit()"></co-icon>
5874
+ }
5875
+ @if (showAdd || showEdit) {
5876
+ <co-icon [iconData]="iconsService.getIcon(icons.PlusSolid)" [title]="'add'" (click)="addClick.emit()"></co-icon>
5877
+ }
5878
+ @if (showDelete) {
5879
+ <co-icon [iconData]="iconsService.getIcon(icons.TrashCanSolid)" [title]="'delete'" [class.disabled]="!deleteEnabled" (click)="handleDeleteClick()"></co-icon>
5880
+ }
5881
+ </div>
5847
5882
  `,
5848
5883
  encapsulation: ViewEncapsulation.None,
5849
5884
  standalone: false
@@ -7164,10 +7199,71 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
7164
7199
  args: ["class.co-calendar"]
7165
7200
  }] } });
7166
7201
 
7202
+ /**
7203
+ * Resolves the locale-specific decimal separator (e.g. ',' for nl-NL) from the app-wide LOCALE_ID and
7204
+ * converts between a numeric model value and its user-facing display string. Injected app-wide, so no
7205
+ * component has to be given a locale explicitly.
7206
+ *
7207
+ * The model always stays a real number (dot-based / JSON-safe); the separator only affects the display
7208
+ * string shown in the input, never the stored value.
7209
+ */
7210
+ class NumberLocaleService {
7211
+ _decimalSeparator;
7212
+ constructor(localeId) {
7213
+ // Intl uses the locale string directly, so this works without registerLocaleData().
7214
+ const parts = new Intl.NumberFormat(localeId).formatToParts(1.1);
7215
+ const decimalPart = parts.find((part) => part.type === 'decimal');
7216
+ this._decimalSeparator = decimalPart ? decimalPart.value : '.';
7217
+ }
7218
+ get decimalSeparator() {
7219
+ return this._decimalSeparator;
7220
+ }
7221
+ // Parses a user-typed display string to a number. Accepts both ',' and '.' as decimal separator so the
7222
+ // field is forgiving. Returns NaN when the text is not a parsable number.
7223
+ parse(text) {
7224
+ if (text === null || text === undefined) {
7225
+ return NaN;
7226
+ }
7227
+ const normalized = ('' + text).trim().replace(',', '.');
7228
+ if (normalized === '') {
7229
+ return NaN;
7230
+ }
7231
+ return Number(normalized);
7232
+ }
7233
+ // Formats a numeric model value to a display string using the locale decimal separator. Returns '' for
7234
+ // null/undefined/NaN. No grouping (thousands) separators are added, as this targets an editable field.
7235
+ format(value) {
7236
+ if (value === null || value === undefined || isNaN(value)) {
7237
+ return '';
7238
+ }
7239
+ return ('' + value).replace('.', this._decimalSeparator);
7240
+ }
7241
+ // Inserts the locale decimal separator at the input's caret and notifies Angular via an 'input' event.
7242
+ // Used to normalize a typed '.' to the locale separator (e.g. numpad '.' -> ',' for nl-NL).
7243
+ insertDecimalSeparator(input) {
7244
+ const start = input.selectionStart ?? input.value.length;
7245
+ const end = input.selectionEnd ?? input.value.length;
7246
+ input.value = input.value.slice(0, start) + this._decimalSeparator + input.value.slice(end);
7247
+ const caret = start + this._decimalSeparator.length;
7248
+ input.setSelectionRange(caret, caret);
7249
+ input.dispatchEvent(new Event('input', { bubbles: true }));
7250
+ }
7251
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NumberLocaleService, deps: [{ token: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Injectable });
7252
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NumberLocaleService, providedIn: 'root' });
7253
+ }
7254
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NumberLocaleService, decorators: [{
7255
+ type: Injectable,
7256
+ args: [{ providedIn: 'root' }]
7257
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
7258
+ type: Inject,
7259
+ args: [LOCALE_ID]
7260
+ }] }] });
7261
+
7167
7262
  class InputTextComponent extends BaseInputComponent {
7168
7263
  formComponent;
7169
7264
  changeDetector;
7170
7265
  overlayService;
7266
+ _numberLocale;
7171
7267
  formUserChangeListener;
7172
7268
  ngZoneWrapper;
7173
7269
  elementRef;
@@ -7196,6 +7292,9 @@ class InputTextComponent extends BaseInputComponent {
7196
7292
  get isDate() {
7197
7293
  return this.type === "date";
7198
7294
  }
7295
+ get hasValue() {
7296
+ return super.hasValue || (this.placeholder !== '' && this.placeholder !== null && this.placeholder !== undefined);
7297
+ }
7199
7298
  leftIconClick = new EventEmitter();
7200
7299
  leftIconMouseDown = new EventEmitter();
7201
7300
  leftIconMouseUp = new EventEmitter();
@@ -7240,6 +7339,26 @@ class InputTextComponent extends BaseInputComponent {
7240
7339
  get model() {
7241
7340
  return super.model;
7242
7341
  }
7342
+ // keep the display string in sync when the model changes from outside typing (init / programmatic)
7343
+ modelSet() {
7344
+ if (this.digitsOnly && !this._userTyping) {
7345
+ this._displayValue = this._numberLocale.format(this.model);
7346
+ }
7347
+ }
7348
+ // For digitsOnly the input is a text field showing a locale-formatted string; parse it back to a number
7349
+ // before emitting so the model (and thus what is stored) stays numeric. Other types emit the raw value.
7350
+ handleInputModelChange(value) {
7351
+ if (this.digitsOnly) {
7352
+ this._userTyping = true;
7353
+ this._displayValue = value === null || value === undefined ? '' : '' + value;
7354
+ const parsed = this._numberLocale.parse(value);
7355
+ this.modelChange.emit(isNaN(parsed) ? null : parsed);
7356
+ this._userTyping = false;
7357
+ }
7358
+ else {
7359
+ this.modelChange.emit(value);
7360
+ }
7361
+ }
7243
7362
  get pipedModel() {
7244
7363
  if (this.formatPipe) {
7245
7364
  try {
@@ -7251,21 +7370,37 @@ class InputTextComponent extends BaseInputComponent {
7251
7370
  }
7252
7371
  return this.model;
7253
7372
  }
7373
+ // Locale-formatted string bound to the input when digitsOnly (comma for nl-NL); the model stays numeric.
7374
+ get displayValue() {
7375
+ return this._displayValue;
7376
+ }
7254
7377
  isFocusedOnDate = false;
7255
7378
  weekInputBuffer = '';
7256
7379
  isWeekInputMode = false;
7257
7380
  _isLeftIconMouseDown = false;
7258
7381
  _isRightIconMouseDown = false;
7259
- constructor(formComponent, changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef) {
7382
+ _userTyping = false;
7383
+ _displayValue = '';
7384
+ constructor(formComponent, changeDetector, overlayService, _numberLocale, formUserChangeListener, ngZoneWrapper, elementRef) {
7260
7385
  super(changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef);
7261
7386
  this.formComponent = formComponent;
7262
7387
  this.changeDetector = changeDetector;
7263
7388
  this.overlayService = overlayService;
7389
+ this._numberLocale = _numberLocale;
7264
7390
  this.formUserChangeListener = formUserChangeListener;
7265
7391
  this.ngZoneWrapper = ngZoneWrapper;
7266
7392
  this.elementRef = elementRef;
7267
7393
  super._markAsOnPush();
7268
7394
  }
7395
+ ngAfterViewInit() {
7396
+ // label was not correctly being used, so this is a legacy issue fix
7397
+ // if you want to use a placeholder (as a placeholder is meant to be used) then you need to set the label as well
7398
+ if (!this.label) {
7399
+ this.label = this.placeholder;
7400
+ this.placeholder = '';
7401
+ }
7402
+ super.ngAfterViewInit();
7403
+ }
7269
7404
  handleLeftIconClick(event) {
7270
7405
  event.preventDefault();
7271
7406
  event.stopPropagation();
@@ -7331,6 +7466,10 @@ class InputTextComponent extends BaseInputComponent {
7331
7466
  if (this.type === 'date') {
7332
7467
  this.isFocusedOnDate = false;
7333
7468
  }
7469
+ if (this.digitsOnly) {
7470
+ // normalize the display once editing stops (e.g. "12," -> "12")
7471
+ this._displayValue = this._numberLocale.format(this.model);
7472
+ }
7334
7473
  this.isFocused.emit(false);
7335
7474
  }
7336
7475
  handleDoFocus(event) {
@@ -7338,10 +7477,17 @@ class InputTextComponent extends BaseInputComponent {
7338
7477
  this.doFocus(event);
7339
7478
  }
7340
7479
  }
7480
+ // For digitsOnly the field is a text input and precision/scale is enforced in keydown; the native
7481
+ // maxlength would wrongly count the decimal separator as a character. Plain text keeps native maxlength.
7482
+ _setNativeMaxLength(maxlength) {
7483
+ if (this.digitsOnly) {
7484
+ return;
7485
+ }
7486
+ super._setNativeMaxLength(maxlength);
7487
+ }
7341
7488
  clearInput(event) {
7342
7489
  this.setModel(null);
7343
7490
  this.keepFocus = true;
7344
- this.shouldCommit = false;
7345
7491
  this.clearIconClick.emit(event);
7346
7492
  }
7347
7493
  handleKeyDownInput(event) {
@@ -7377,16 +7523,39 @@ class InputTextComponent extends BaseInputComponent {
7377
7523
  }
7378
7524
  }
7379
7525
  if (this.digitsOnly && !this.isWeekInputMode) {
7380
- const excludedKeys = this.excludePlusMinus ? ['e', '-', '+'] : ['e'];
7381
- if (excludedKeys.includes(event.key)) {
7382
- event.preventDefault();
7526
+ // the field is a text input (so a locale comma is allowed), so actively permit only numeric characters
7527
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
7528
+ const isDigit = /\d/.test(event.key);
7529
+ const isSeparator = event.key === '.' || event.key === ',';
7530
+ const isSign = !this.excludePlusMinus && (event.key === '-' || event.key === '+');
7531
+ if (!isDigit && !isSeparator && !isSign) {
7532
+ event.preventDefault();
7533
+ return;
7534
+ }
7535
+ if (isSeparator) {
7536
+ // only one decimal separator, and only the locale one: normalize a foreign '.'/',' to it
7537
+ if (/[.,]/.test(this.input?.value ?? '')) {
7538
+ event.preventDefault();
7539
+ return;
7540
+ }
7541
+ if (event.key !== this._numberLocale.decimalSeparator) {
7542
+ event.preventDefault();
7543
+ if (this.input) {
7544
+ this._numberLocale.insertDecimalSeparator(this.input);
7545
+ }
7546
+ return;
7547
+ }
7548
+ }
7383
7549
  }
7384
7550
  }
7385
- const value = String(this.model ?? "");
7386
- if (this.type === "number" &&
7387
- this.maxLength > 0 &&
7388
- value.length >= this.maxLength &&
7389
- /^\d$/.test(key)) {
7551
+ // Native <input> ignores maxlength on numeric inputs, so enforce maxLength (as precision) and
7552
+ // decimals (as scale) ourselves for digit keys. Non-numeric text inputs keep the native maxlength.
7553
+ // Read the live element value (not this.model): a numeric input reports "" for an in-progress value
7554
+ // like "1234567.", so this.model would still hold "1234567" and wrongly block the first decimal digit.
7555
+ const currentText = this.input ? this.input.value : String(this.model ?? "");
7556
+ if ((this.digitsOnly || this.type === "number") &&
7557
+ /^\d$/.test(key) &&
7558
+ NumberUtils.WouldExceedDigitLimits(currentText, this.maxLength, this.decimals)) {
7390
7559
  event.preventDefault();
7391
7560
  }
7392
7561
  }
@@ -7436,7 +7605,7 @@ class InputTextComponent extends BaseInputComponent {
7436
7605
  const day = String(date.getDate()).padStart(2, '0');
7437
7606
  return `${year}-${month}-${day}`;
7438
7607
  }
7439
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: InputTextComponent, deps: [{ token: FormComponent, optional: true }, { token: i0.ChangeDetectorRef }, { token: OverlayService }, { token: FormInputUserModelChangeListenerService }, { token: NgZoneWrapperService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
7608
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: InputTextComponent, deps: [{ token: FormComponent, optional: true }, { token: i0.ChangeDetectorRef }, { token: OverlayService }, { token: NumberLocaleService }, { token: FormInputUserModelChangeListenerService }, { token: NgZoneWrapperService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
7440
7609
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: InputTextComponent, isStandalone: false, selector: "co-input-text", inputs: { useContent: "useContent", placeholder: "placeholder", align: "align", type: "type", formatPipe: "formatPipe", min: "min", max: "max", pattern: "pattern", digitsOnly: "digitsOnly", excludePlusMinus: "excludePlusMinus", showClearButton: "showClearButton", keyDownWhiteList: "keyDownWhiteList", showPlaceholderOnFocus: "showPlaceholderOnFocus", leftIcon: "leftIcon", rightIcon: "rightIcon", leftIconData: "leftIconData", rightIconData: "rightIconData", selectOnFocus: "selectOnFocus", emptyPlace: "emptyPlace", firstDayOfWeek: "firstDayOfWeek", noStyle: "noStyle", hideArrowButtons: "hideArrowButtons", model: "model" }, outputs: { leftIconClick: "leftIconClick", leftIconMouseDown: "leftIconMouseDown", leftIconMouseUp: "leftIconMouseUp", rightIconClick: "rightIconClick", rightIconMouseDown: "rightIconMouseDown", rightIconMouseUp: "rightIconMouseUp", clearIconClick: "clearIconClick", isFocused: "isFocused" }, host: { listeners: { "document:pointerup": "handleDocumentPointerUp($event)", "document:pointercancel": "handleDocumentPointerUp($event)" }, properties: { "class.no-style": "this.noStyle", "class.hide-arrows": "this.hideArrowButtons", "class.isDate": "this.isDate", "class.co-input-text": "this.showClass", "class.has-left-icon": "this.hasLeftIcon", "class.has-right-icon": "this.hasRightIcon", "class.has-own-label": "this.hasOwnLabel" } }, providers: [
7441
7610
  OverlayService, {
7442
7611
  provide: SCREEN_CONFIG_ADAPTER_COMPONENT_INTERFACE_NAME,
@@ -7458,7 +7627,7 @@ class InputTextComponent extends BaseInputComponent {
7458
7627
  <div class="input-wrapper">
7459
7628
  @if (showPlaceholderOnFocus || (!showPlaceholderOnFocus && !hasValue && !focused)) {
7460
7629
  <label
7461
- [textContent]="placeholder"></label>
7630
+ [textContent]="label"></label>
7462
7631
  }
7463
7632
  @if (!focused && !useContent && formatPipe) {
7464
7633
  <span class="input-text-formatted"
@@ -7471,15 +7640,16 @@ class InputTextComponent extends BaseInputComponent {
7471
7640
  <input [class.show]="focused || !formatPipe" #input
7472
7641
  [class.input-input-hidden]="useContent"
7473
7642
  [ngClass]="align"
7474
- [type]="isWeekInputMode ? 'text' : (isFocusedOnDate || (hasValue && emptyPlace)) ? 'date' : (digitsOnly ? 'number' : (type === 'date' ? 'text' : type))"
7475
- [placeholder]="type === 'date' && !isFocusedOnDate ? '' : ''"
7643
+ [type]="isWeekInputMode ? 'text' : (isFocusedOnDate || (hasValue && emptyPlace)) ? 'date' : (digitsOnly ? 'text' : (type === 'date' ? 'text' : type))"
7644
+ [attr.inputmode]="digitsOnly ? 'decimal' : null"
7645
+ [placeholder]="type === 'date' && !isFocusedOnDate ? '' : placeholder"
7476
7646
  [pattern]="type === 'date' ? pattern : undefined"
7477
- [ngModel]="model"
7647
+ [ngModel]="digitsOnly ? displayValue : model"
7478
7648
  [min]="(type === 'number' || type === 'date') && this.min ? this.min : undefined"
7479
7649
  [max]="(type === 'number' || type === 'date') && this.max ? this.max : undefined"
7480
7650
  [readonly]="readonly"
7481
7651
  [required]="required"
7482
- (ngModelChange)="modelChange.emit($event)"
7652
+ (ngModelChange)="handleInputModelChange($event)"
7483
7653
  (keydown)="handleKeyDownInput($event)"
7484
7654
  (keyup)="keyUp.emit($event)"
7485
7655
  (focusin)="handleInputFocus($event)"
@@ -7534,7 +7704,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
7534
7704
  <div class="input-wrapper">
7535
7705
  @if (showPlaceholderOnFocus || (!showPlaceholderOnFocus && !hasValue && !focused)) {
7536
7706
  <label
7537
- [textContent]="placeholder"></label>
7707
+ [textContent]="label"></label>
7538
7708
  }
7539
7709
  @if (!focused && !useContent && formatPipe) {
7540
7710
  <span class="input-text-formatted"
@@ -7547,15 +7717,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
7547
7717
  <input [class.show]="focused || !formatPipe" #input
7548
7718
  [class.input-input-hidden]="useContent"
7549
7719
  [ngClass]="align"
7550
- [type]="isWeekInputMode ? 'text' : (isFocusedOnDate || (hasValue && emptyPlace)) ? 'date' : (digitsOnly ? 'number' : (type === 'date' ? 'text' : type))"
7551
- [placeholder]="type === 'date' && !isFocusedOnDate ? '' : ''"
7720
+ [type]="isWeekInputMode ? 'text' : (isFocusedOnDate || (hasValue && emptyPlace)) ? 'date' : (digitsOnly ? 'text' : (type === 'date' ? 'text' : type))"
7721
+ [attr.inputmode]="digitsOnly ? 'decimal' : null"
7722
+ [placeholder]="type === 'date' && !isFocusedOnDate ? '' : placeholder"
7552
7723
  [pattern]="type === 'date' ? pattern : undefined"
7553
- [ngModel]="model"
7724
+ [ngModel]="digitsOnly ? displayValue : model"
7554
7725
  [min]="(type === 'number' || type === 'date') && this.min ? this.min : undefined"
7555
7726
  [max]="(type === 'number' || type === 'date') && this.max ? this.max : undefined"
7556
7727
  [readonly]="readonly"
7557
7728
  [required]="required"
7558
- (ngModelChange)="modelChange.emit($event)"
7729
+ (ngModelChange)="handleInputModelChange($event)"
7559
7730
  (keydown)="handleKeyDownInput($event)"
7560
7731
  (keyup)="keyUp.emit($event)"
7561
7732
  (focusin)="handleInputFocus($event)"
@@ -7606,7 +7777,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
7606
7777
  }]
7607
7778
  }], ctorParameters: () => [{ type: FormComponent, decorators: [{
7608
7779
  type: Optional
7609
- }] }, { type: i0.ChangeDetectorRef }, { type: OverlayService }, { type: FormInputUserModelChangeListenerService }, { type: NgZoneWrapperService }, { type: i0.ElementRef }], propDecorators: { useContent: [{
7780
+ }] }, { type: i0.ChangeDetectorRef }, { type: OverlayService }, { type: NumberLocaleService }, { type: FormInputUserModelChangeListenerService }, { type: NgZoneWrapperService }, { type: i0.ElementRef }], propDecorators: { useContent: [{
7610
7781
  type: Input
7611
7782
  }], placeholder: [{
7612
7783
  type: Input
@@ -9035,16 +9206,25 @@ class InputNumberPickerComponent extends BaseInputComponent {
9035
9206
  iconCacheService;
9036
9207
  overlayService;
9037
9208
  _ngZone;
9209
+ _numberLocale;
9038
9210
  ngZoneWrapper;
9039
9211
  elementRef;
9040
9212
  // @override
9041
9213
  set model(model) {
9042
9214
  this.setValue(model, true);
9043
9215
  super.model = model;
9216
+ // keep the display string in sync, unless the user is mid-typing (then their raw text is preserved)
9217
+ if (!this._userTyping) {
9218
+ this._displayValue = this._numberLocale.format(model);
9219
+ }
9044
9220
  }
9045
9221
  get model() {
9046
9222
  return super.model;
9047
9223
  }
9224
+ // Locale-formatted string bound to the native text input (comma for nl-NL); the model stays numeric.
9225
+ get displayValue() {
9226
+ return this._displayValue;
9227
+ }
9048
9228
  modelChangeOnEnter = true;
9049
9229
  showPermanentLabel = false;
9050
9230
  leftIconData;
@@ -9065,8 +9245,14 @@ class InputNumberPickerComponent extends BaseInputComponent {
9065
9245
  // @override Default true for us.
9066
9246
  noValidation = true;
9067
9247
  set decimals(decimals) {
9248
+ // Also store on the base so its precision/scale validator picks it up; a setter-only override
9249
+ // would otherwise shadow the inherited getter and make this.decimals read as undefined.
9250
+ super.decimals = decimals;
9068
9251
  this.numberLogic.decimals = decimals;
9069
9252
  }
9253
+ get decimals() {
9254
+ return super.decimals;
9255
+ }
9070
9256
  // @override
9071
9257
  modelChange = new EventEmitter();
9072
9258
  iconClick = new EventEmitter();
@@ -9086,6 +9272,8 @@ class InputNumberPickerComponent extends BaseInputComponent {
9086
9272
  plusSelected = false;
9087
9273
  _numberInputHasFocus = false;
9088
9274
  _changeFromButton = false;
9275
+ _userTyping = false;
9276
+ _displayValue = '';
9089
9277
  _numberLogicValueChangeSub;
9090
9278
  _delayBeforeStartAutoCountMs = 666;
9091
9279
  // the 'speed gears' for auto counting
@@ -9100,12 +9288,13 @@ class InputNumberPickerComponent extends BaseInputComponent {
9100
9288
  _autoCountTimeout;
9101
9289
  _stepIncrementTimeout;
9102
9290
  _startAutocountTimeout;
9103
- constructor(formComponent, iconCacheService, overlayService, _ngZone, changeDetector, formUserChangeListener, ngZoneWrapper, elementRef) {
9291
+ constructor(formComponent, iconCacheService, overlayService, _ngZone, changeDetector, formUserChangeListener, _numberLocale, ngZoneWrapper, elementRef) {
9104
9292
  super(changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef);
9105
9293
  this.formComponent = formComponent;
9106
9294
  this.iconCacheService = iconCacheService;
9107
9295
  this.overlayService = overlayService;
9108
9296
  this._ngZone = _ngZone;
9297
+ this._numberLocale = _numberLocale;
9109
9298
  this.ngZoneWrapper = ngZoneWrapper;
9110
9299
  this.elementRef = elementRef;
9111
9300
  this._numberLogicValueChangeSub = this.numberLogic.valueChange.subscribe((value) => {
@@ -9155,13 +9344,40 @@ class InputNumberPickerComponent extends BaseInputComponent {
9155
9344
  if (this.myKeyDownWhiteList.find((k) => k === event.keyCode) === undefined) {
9156
9345
  return false;
9157
9346
  }
9347
+ const currentText = this.input ? this.input.value : String(this.model ?? '');
9348
+ if (event.key === '.' || event.key === ',') {
9349
+ // only one decimal separator, and only the locale one: normalize a foreign '.'/',' to it
9350
+ if (/[.,]/.test(currentText)) {
9351
+ return false;
9352
+ }
9353
+ if (event.key !== this._numberLocale.decimalSeparator) {
9354
+ if (this.input) {
9355
+ this._numberLocale.insertDecimalSeparator(this.input);
9356
+ }
9357
+ return false;
9358
+ }
9359
+ }
9360
+ // native text/number inputs ignore maxlength, so enforce maxLength (precision) and decimals (scale) here
9361
+ if (/^\d$/.test(event.key) && NumberUtils.WouldExceedDigitLimits(currentText, this.maxLength, this.decimals)) {
9362
+ return false;
9363
+ }
9364
+ }
9365
+ // precision/scale is enforced in keydown; the native maxlength on this text input would wrongly count
9366
+ // the decimal separator as a character (e.g. blocking "1234567,89" at maxLength 9), so don't set it.
9367
+ _setNativeMaxLength() {
9158
9368
  }
9159
9369
  handleBlur() {
9370
+ // normalize the display once editing stops (e.g. "12," -> "12", trailing zeros dropped)
9371
+ this._displayValue = this._numberLocale.format(this.model);
9160
9372
  this.modelChange.next(this.model);
9161
9373
  }
9162
- handleChangeModel(value) {
9374
+ handleChangeModel(text) {
9163
9375
  this._changeFromButton = false;
9164
- this.numberLogic.setValue(value);
9376
+ // preserve exactly what the user typed while they are typing (avoids caret jumps / eating the separator)
9377
+ this._userTyping = true;
9378
+ this._displayValue = text === null || text === undefined ? '' : '' + text;
9379
+ this.numberLogic.setValue(this._numberLocale.parse(text));
9380
+ this._userTyping = false;
9165
9381
  }
9166
9382
  // Note: recursive through setTimeout().
9167
9383
  doDecrementAuto() {
@@ -9263,7 +9479,7 @@ class InputNumberPickerComponent extends BaseInputComponent {
9263
9479
  break;
9264
9480
  }
9265
9481
  }
9266
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: InputNumberPickerComponent, deps: [{ token: FormComponent, optional: true }, { token: IconCacheService }, { token: OverlayService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: FormInputUserModelChangeListenerService }, { token: NgZoneWrapperService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
9482
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: InputNumberPickerComponent, deps: [{ token: FormComponent, optional: true }, { token: IconCacheService }, { token: OverlayService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: FormInputUserModelChangeListenerService }, { token: NumberLocaleService }, { token: NgZoneWrapperService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
9267
9483
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: InputNumberPickerComponent, isStandalone: false, selector: "co-input-number-picker", inputs: { model: "model", modelChangeOnEnter: "modelChangeOnEnter", showPermanentLabel: "showPermanentLabel", leftIconData: "leftIconData", min: "min", step: "step", max: "max", ngModelOptions: "ngModelOptions", minusIcon: "minusIcon", plusIcon: "plusIcon", buttonShowMode: "buttonShowMode", noValidation: "noValidation", decimals: "decimals" }, outputs: { modelChange: "modelChange", iconClick: "iconClick" }, host: { properties: { "class.has-icon": "this.leftIconData", "class.show-buttons-on-focus-only": "this.showButtonsOnFocusOnly", "class.has-label": "this.hasLabel", "class.co-input-number-picker": "this.showClass" } }, providers: [
9268
9484
  OverlayService, {
9269
9485
  provide: SCREEN_CONFIG_ADAPTER_COMPONENT_INTERFACE_NAME, useExisting: forwardRef(() => InputNumberPickerComponent)
@@ -9289,15 +9505,15 @@ class InputNumberPickerComponent extends BaseInputComponent {
9289
9505
  </div>
9290
9506
  <div class="input-wrapper">
9291
9507
  @if (showPermanentLabel) {
9292
- <span class='permanent-label' [textContent]="label"></span>
9508
+ <span class='permanent-label' [textContent]="label ?? ''"></span>
9293
9509
  }
9294
- <input type="number"
9510
+ <input type="text" inputmode="decimal"
9295
9511
  [tabIndex]="readonly ? -1 : 0"
9296
- [ngModel]="model"
9512
+ [ngModel]="displayValue"
9297
9513
  [readonly]="readonly"
9298
9514
  [disabled]="disabled"
9299
9515
  [required]="required"
9300
- [placeholder]="label"
9516
+ [placeholder]="label ?? ''"
9301
9517
  (ngModelChange)="handleChangeModel($event)"
9302
9518
  (keydown)="handleInputKeyDown($event)"
9303
9519
  (blur)="handleBlur()"/>
@@ -9311,7 +9527,7 @@ class InputNumberPickerComponent extends BaseInputComponent {
9311
9527
  (mouseup)="stopAutoCounting()" (mouseleave)="stopAutoCounting()"></co-button>
9312
9528
  }
9313
9529
  </div>
9314
- `, isInline: true, dependencies: [{ kind: "directive", type: i5.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: i5.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ButtonComponent, selector: "co-button", inputs: ["label", "iconData", "iconDataRight", "isToggleButton", "isToggled", "hidden", "disabled"], outputs: ["onClick", "clickedWhileDisabled", "isToggledChange"] }, { kind: "component", type: IconComponent, selector: "co-icon", inputs: ["icon", "iconData"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
9530
+ `, isInline: true, dependencies: [{ kind: "directive", type: i5.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: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ButtonComponent, selector: "co-button", inputs: ["label", "iconData", "iconDataRight", "isToggleButton", "isToggled", "hidden", "disabled"], outputs: ["onClick", "clickedWhileDisabled", "isToggledChange"] }, { kind: "component", type: IconComponent, selector: "co-icon", inputs: ["icon", "iconData"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
9315
9531
  }
9316
9532
  __decorate([
9317
9533
  InputBoolean()
@@ -9338,15 +9554,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9338
9554
  </div>
9339
9555
  <div class="input-wrapper">
9340
9556
  @if (showPermanentLabel) {
9341
- <span class='permanent-label' [textContent]="label"></span>
9557
+ <span class='permanent-label' [textContent]="label ?? ''"></span>
9342
9558
  }
9343
- <input type="number"
9559
+ <input type="text" inputmode="decimal"
9344
9560
  [tabIndex]="readonly ? -1 : 0"
9345
- [ngModel]="model"
9561
+ [ngModel]="displayValue"
9346
9562
  [readonly]="readonly"
9347
9563
  [disabled]="disabled"
9348
9564
  [required]="required"
9349
- [placeholder]="label"
9565
+ [placeholder]="label ?? ''"
9350
9566
  (ngModelChange)="handleChangeModel($event)"
9351
9567
  (keydown)="handleInputKeyDown($event)"
9352
9568
  (blur)="handleBlur()"/>
@@ -9378,7 +9594,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9378
9594
  }] }, { type: IconCacheService, decorators: [{
9379
9595
  type: Inject,
9380
9596
  args: [IconCacheService]
9381
- }] }, { type: OverlayService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: FormInputUserModelChangeListenerService }, { type: NgZoneWrapperService }, { type: i0.ElementRef }], propDecorators: { model: [{
9597
+ }] }, { type: OverlayService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: FormInputUserModelChangeListenerService }, { type: NumberLocaleService }, { type: NgZoneWrapperService }, { type: i0.ElementRef }], propDecorators: { model: [{
9382
9598
  type: Input
9383
9599
  }], modelChangeOnEnter: [{
9384
9600
  type: Input
@@ -13527,6 +13743,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
13527
13743
  }]
13528
13744
  }] });
13529
13745
 
13746
+ class LovUtils {
13747
+ static getDisplayValue(model, displayField) {
13748
+ if (!model || !displayField) {
13749
+ return '';
13750
+ }
13751
+ return displayField
13752
+ .split('|')
13753
+ .map(field => model[field.trim()])
13754
+ .filter(val => val != null && val !== '')
13755
+ .join(' - ');
13756
+ }
13757
+ static getHighlightParts(value, term) {
13758
+ const text = value ?? '';
13759
+ if (!text || !term) {
13760
+ return [{ text: text, match: false }];
13761
+ }
13762
+ const regex = new RegExp(LovUtils._escapeRegExp(term), 'gi');
13763
+ const parts = [];
13764
+ let lastIndex = 0;
13765
+ let match;
13766
+ while ((match = regex.exec(text)) !== null) {
13767
+ if (match.index > lastIndex) {
13768
+ parts.push({ text: text.slice(lastIndex, match.index), match: false });
13769
+ }
13770
+ parts.push({ text: match[0], match: true });
13771
+ lastIndex = match.index + match[0].length;
13772
+ if (match.index === regex.lastIndex) {
13773
+ regex.lastIndex++;
13774
+ }
13775
+ }
13776
+ if (lastIndex < text.length) {
13777
+ parts.push({ text: text.slice(lastIndex), match: false });
13778
+ }
13779
+ return parts;
13780
+ }
13781
+ static _escapeRegExp(value) {
13782
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13783
+ }
13784
+ }
13785
+
13530
13786
  class ListOfValuesPopupComponent {
13531
13787
  iconCacheService;
13532
13788
  _elementRef;
@@ -13647,7 +13903,11 @@ class ListOfValuesPopupComponent {
13647
13903
  }
13648
13904
  }
13649
13905
  selectModelByViewModel(viewModel, closePopup = true) {
13650
- this.model = this.viewModelsMain.find(vm => vm === viewModel).model;
13906
+ const found = this.viewModelsMain.find(vm => vm === viewModel);
13907
+ if (!found) {
13908
+ return;
13909
+ }
13910
+ this.model = found.model;
13651
13911
  this.modelChange.emit(this.model);
13652
13912
  this._scrollIntoView();
13653
13913
  if (closePopup) {
@@ -13660,7 +13920,7 @@ class ListOfValuesPopupComponent {
13660
13920
  this.selectNextOption();
13661
13921
  }
13662
13922
  if (!this.model && this.searchTerm) {
13663
- const wishModel = this.viewModelsMain.find(vmm => vmm.model[this.displayField] === this.searchTerm);
13923
+ const wishModel = this.viewModelsMain.find(vmm => LovUtils.getDisplayValue(vmm.model, this.displayField) === this.searchTerm);
13664
13924
  if (wishModel) {
13665
13925
  this.selectViewModel(wishModel);
13666
13926
  }
@@ -13680,6 +13940,9 @@ class ListOfValuesPopupComponent {
13680
13940
  this.modelChange.emit(this.model);
13681
13941
  }
13682
13942
  selectNextOption(back = false) {
13943
+ if (this.viewModels.length === 0) {
13944
+ return;
13945
+ }
13683
13946
  let nextModel;
13684
13947
  if (!this.highLightModel) {
13685
13948
  nextModel = this.viewModels[back ? this.viewModels.length - 1 : 0];
@@ -13733,8 +13996,8 @@ class ListOfValuesPopupComponent {
13733
13996
  }
13734
13997
  if (!this.model)
13735
13998
  return false;
13736
- const selected = this.model?.[this.displayField];
13737
- const current = vm.model?.[this.displayField];
13999
+ const selected = LovUtils.getDisplayValue(this.model, this.displayField);
14000
+ const current = LovUtils.getDisplayValue(vm.model, this.displayField);
13738
14001
  return selected != null && current != null && selected === current;
13739
14002
  }
13740
14003
  _prepareViewModelsMain() {
@@ -13755,6 +14018,9 @@ class ListOfValuesPopupComponent {
13755
14018
  }
13756
14019
  });
13757
14020
  });
14021
+ this.viewModels.forEach((vm) => {
14022
+ vm.highlightParts = LovUtils.getHighlightParts(LovUtils.getDisplayValue(vm.model, this.displayField), this.searchTerm);
14023
+ });
13758
14024
  }
13759
14025
  _scrollIntoView() {
13760
14026
  const activeIndex = this.viewModels.findIndex(vmm => vmm === this.highLightModel);
@@ -13783,102 +14049,126 @@ class ListOfValuesPopupComponent {
13783
14049
  }
13784
14050
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ListOfValuesPopupComponent, deps: [{ token: IconCacheService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
13785
14051
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: ListOfValuesPopupComponent, isStandalone: false, selector: "co-list-of-values-popup", inputs: { model: "model", multiselect: "multiselect", showToggleAll: "showToggleAll", displayField: "displayField", searchPlaceholder: "searchPlaceholder", customCssClass: "customCssClass", searchDisabled: "searchDisabled", parentForOverlay: "parentForOverlay", optionIcon: "optionIcon", collection: "collection" }, outputs: { modelChange: "modelChange", closePopup: "closePopup", keyDown: "keyDown" }, host: { properties: { "class.co-list-of-values-popup": "this.showClass", "class.lov-anim": "this.animBase", "class.lov-anim--open": "this.isOpen" } }, viewQueries: [{ propertyName: "dropDownList", first: true, predicate: ["dropDownList"], descendants: true, read: ElementRef }, { propertyName: "inputSearch", first: true, predicate: InputSearchComponent, descendants: true }, { propertyName: "lovItems", predicate: ["lovItem"], descendants: true, read: ElementRef }], ngImport: i0, template: `
13786
- <div class="lov-options" [overlay]="parentForOverlay" [inheritWidth]="true" [ngClass]="customCssClass"
13787
- id="lov-popup"
13788
- role="listbox" [tabindex]="-1"
13789
- (clickOutside)="closePopup.emit($event)">
13790
- @if (multiselect) {
13791
- <co-input-search
13792
- tabindex="-1"
13793
- [(model)]="searchTerm"
13794
- [placeholder]="searchPlaceholder"
13795
- (keydown)="handleInputKeyDown($event)"
13796
- (modelChange)="filterViewModels()"></co-input-search>
13797
- }
13798
- @if (showToggleAll && multiselect) {
13799
- <div class="row gap">
13800
- <co-input-checkbox [model]="allSelected" (modelChange)="toggleAll()"></co-input-checkbox>
13801
- <span [textContent]="'DESELECT_ALL' | coreLocalize" (click)="toggleAll()"></span>
13802
- </div>
13803
- }
13804
- <ul class="dropdown-list" #dropDownList>
13805
- @for (viewModel of viewModels; track viewModel; let index = $index) {
13806
- <li
13807
- #lovItem
13808
- [class.selected]="viewModel === highLightModel || viewModels.length === 1"
13809
- [class.active]="isActive(viewModel)"
13810
- [class.highlighted]="viewModel === highLightModel"
13811
- [attr.aria-selected]="isActive(viewModel)"
13812
- (click)="selectViewModel(viewModel, !multiselect)"
13813
- role="option">
13814
- @if (!multiselect) {
13815
- @if (viewModel.model[optionIcon]) {
13816
- <co-icon class="input-text-left-icon" [iconData]="iconCacheService.getIcon(viewModel.model[optionIcon])">
13817
- </co-icon>
14052
+ <div class="lov-options" [overlay]="parentForOverlay" [inheritWidth]="true" [ngClass]="customCssClass"
14053
+ id="lov-popup"
14054
+ role="listbox" [tabindex]="-1"
14055
+ (clickOutside)="closePopup.emit($event)">
14056
+ @if (multiselect) {
14057
+ <co-input-search
14058
+ tabindex="-1"
14059
+ [(model)]="searchTerm"
14060
+ [placeholder]="searchPlaceholder"
14061
+ (keydown)="handleInputKeyDown($event)"
14062
+ (modelChange)="filterViewModels()"></co-input-search>
14063
+ }
14064
+ @if (showToggleAll && multiselect) {
14065
+ <div class="row gap">
14066
+ <co-input-checkbox [model]="allSelected" (modelChange)="toggleAll()"></co-input-checkbox>
14067
+ <span [textContent]="'DESELECT_ALL' | coreLocalize" (click)="toggleAll()"></span>
14068
+ </div>
14069
+ }
14070
+ <ul class="dropdown-list" #dropDownList>
14071
+ @for (viewModel of viewModels; track viewModel; let index = $index) {
14072
+ <li
14073
+ #lovItem
14074
+ [class.selected]="viewModel === highLightModel || viewModels.length === 1"
14075
+ [class.active]="isActive(viewModel)"
14076
+ [class.highlighted]="viewModel === highLightModel"
14077
+ [attr.aria-selected]="isActive(viewModel)"
14078
+ (click)="selectViewModel(viewModel, !multiselect)"
14079
+ role="option">
14080
+ @if (!multiselect) {
14081
+ @if (viewModel.model[optionIcon]) {
14082
+ <co-icon class="input-text-left-icon" [iconData]="iconCacheService.getIcon(viewModel.model[optionIcon])">
14083
+ </co-icon>
14084
+ }
14085
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14086
+ }
14087
+ @if (multiselect) {
14088
+ <co-input-checkbox [model]="viewModel.checked"
14089
+ (modelChange)="selectViewModel(viewModel, false)"></co-input-checkbox>
14090
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14091
+ }
14092
+ </li>
13818
14093
  }
13819
- <span class="lov-options-text" [textContent]="viewModel.model[displayField]"></span>
13820
- }
13821
- @if (multiselect) {
13822
- <co-input-checkbox [model]="viewModel.checked"
13823
- (modelChange)="selectViewModel(viewModel, false)"></co-input-checkbox>
13824
- <span class="lov-options-text" [textContent]="viewModel.model[displayField]"></span>
13825
- }
13826
- </li>
13827
- }
13828
- </ul>
13829
- </div>
13830
- `, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: InputCheckboxComponent, selector: "co-input-checkbox", inputs: ["cssClass", "clickableLabel"], outputs: ["modelChange"] }, { kind: "directive", type: OverlayDirective, selector: "[overlay]", inputs: ["overlay", "view", "inline", "keepInView", "inheritWidth", "rightAlign", "fullSize"] }, { kind: "directive", type: ClickOutsideDirective, selector: "[clickOutside]", inputs: ["clickOutside", "alwaysTrigger"], outputs: ["clickOutside"], exportAs: ["clickOutside"] }, { kind: "component", type: IconComponent, selector: "co-icon", inputs: ["icon", "iconData"] }, { kind: "component", type: InputSearchComponent, selector: "co-input-search", inputs: ["placeholder", "handleKeydown", "useLeftIcon", "useRightIcon", "leftIconData", "rightIconData", "centerLabel"], outputs: ["search", "isFocused", "leftIconClick", "rightIconClick"] }, { kind: "pipe", type: CoreLocalizePipe, name: "coreLocalize" }], encapsulation: i0.ViewEncapsulation.None });
14094
+ </ul>
14095
+ </div>
14096
+
14097
+ <ng-template #optionText let-parts="parts">
14098
+ <span class="lov-options-text">
14099
+ @for (part of parts; track $index) {
14100
+ @if (part.match) {
14101
+ <mark class="lov-highlight" [textContent]="part.text"></mark>
14102
+ } @else {
14103
+ <span [textContent]="part.text"></span>
14104
+ }
14105
+ }
14106
+ </span>
14107
+ </ng-template>
14108
+ `, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: InputCheckboxComponent, selector: "co-input-checkbox", inputs: ["cssClass", "clickableLabel"], outputs: ["modelChange"] }, { kind: "directive", type: OverlayDirective, selector: "[overlay]", inputs: ["overlay", "view", "inline", "keepInView", "inheritWidth", "rightAlign", "fullSize"] }, { kind: "directive", type: ClickOutsideDirective, selector: "[clickOutside]", inputs: ["clickOutside", "alwaysTrigger"], outputs: ["clickOutside"], exportAs: ["clickOutside"] }, { kind: "component", type: IconComponent, selector: "co-icon", inputs: ["icon", "iconData"] }, { kind: "component", type: InputSearchComponent, selector: "co-input-search", inputs: ["placeholder", "handleKeydown", "useLeftIcon", "useRightIcon", "leftIconData", "rightIconData", "centerLabel"], outputs: ["search", "isFocused", "leftIconClick", "rightIconClick"] }, { kind: "pipe", type: CoreLocalizePipe, name: "coreLocalize" }], encapsulation: i0.ViewEncapsulation.None });
13831
14109
  }
13832
14110
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ListOfValuesPopupComponent, decorators: [{
13833
14111
  type: Component,
13834
14112
  args: [{
13835
14113
  selector: 'co-list-of-values-popup',
13836
14114
  template: `
13837
- <div class="lov-options" [overlay]="parentForOverlay" [inheritWidth]="true" [ngClass]="customCssClass"
13838
- id="lov-popup"
13839
- role="listbox" [tabindex]="-1"
13840
- (clickOutside)="closePopup.emit($event)">
13841
- @if (multiselect) {
13842
- <co-input-search
13843
- tabindex="-1"
13844
- [(model)]="searchTerm"
13845
- [placeholder]="searchPlaceholder"
13846
- (keydown)="handleInputKeyDown($event)"
13847
- (modelChange)="filterViewModels()"></co-input-search>
13848
- }
13849
- @if (showToggleAll && multiselect) {
13850
- <div class="row gap">
13851
- <co-input-checkbox [model]="allSelected" (modelChange)="toggleAll()"></co-input-checkbox>
13852
- <span [textContent]="'DESELECT_ALL' | coreLocalize" (click)="toggleAll()"></span>
13853
- </div>
13854
- }
13855
- <ul class="dropdown-list" #dropDownList>
13856
- @for (viewModel of viewModels; track viewModel; let index = $index) {
13857
- <li
13858
- #lovItem
13859
- [class.selected]="viewModel === highLightModel || viewModels.length === 1"
13860
- [class.active]="isActive(viewModel)"
13861
- [class.highlighted]="viewModel === highLightModel"
13862
- [attr.aria-selected]="isActive(viewModel)"
13863
- (click)="selectViewModel(viewModel, !multiselect)"
13864
- role="option">
13865
- @if (!multiselect) {
13866
- @if (viewModel.model[optionIcon]) {
13867
- <co-icon class="input-text-left-icon" [iconData]="iconCacheService.getIcon(viewModel.model[optionIcon])">
13868
- </co-icon>
14115
+ <div class="lov-options" [overlay]="parentForOverlay" [inheritWidth]="true" [ngClass]="customCssClass"
14116
+ id="lov-popup"
14117
+ role="listbox" [tabindex]="-1"
14118
+ (clickOutside)="closePopup.emit($event)">
14119
+ @if (multiselect) {
14120
+ <co-input-search
14121
+ tabindex="-1"
14122
+ [(model)]="searchTerm"
14123
+ [placeholder]="searchPlaceholder"
14124
+ (keydown)="handleInputKeyDown($event)"
14125
+ (modelChange)="filterViewModels()"></co-input-search>
14126
+ }
14127
+ @if (showToggleAll && multiselect) {
14128
+ <div class="row gap">
14129
+ <co-input-checkbox [model]="allSelected" (modelChange)="toggleAll()"></co-input-checkbox>
14130
+ <span [textContent]="'DESELECT_ALL' | coreLocalize" (click)="toggleAll()"></span>
14131
+ </div>
14132
+ }
14133
+ <ul class="dropdown-list" #dropDownList>
14134
+ @for (viewModel of viewModels; track viewModel; let index = $index) {
14135
+ <li
14136
+ #lovItem
14137
+ [class.selected]="viewModel === highLightModel || viewModels.length === 1"
14138
+ [class.active]="isActive(viewModel)"
14139
+ [class.highlighted]="viewModel === highLightModel"
14140
+ [attr.aria-selected]="isActive(viewModel)"
14141
+ (click)="selectViewModel(viewModel, !multiselect)"
14142
+ role="option">
14143
+ @if (!multiselect) {
14144
+ @if (viewModel.model[optionIcon]) {
14145
+ <co-icon class="input-text-left-icon" [iconData]="iconCacheService.getIcon(viewModel.model[optionIcon])">
14146
+ </co-icon>
14147
+ }
14148
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14149
+ }
14150
+ @if (multiselect) {
14151
+ <co-input-checkbox [model]="viewModel.checked"
14152
+ (modelChange)="selectViewModel(viewModel, false)"></co-input-checkbox>
14153
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14154
+ }
14155
+ </li>
13869
14156
  }
13870
- <span class="lov-options-text" [textContent]="viewModel.model[displayField]"></span>
13871
- }
13872
- @if (multiselect) {
13873
- <co-input-checkbox [model]="viewModel.checked"
13874
- (modelChange)="selectViewModel(viewModel, false)"></co-input-checkbox>
13875
- <span class="lov-options-text" [textContent]="viewModel.model[displayField]"></span>
13876
- }
13877
- </li>
13878
- }
13879
- </ul>
13880
- </div>
13881
- `,
14157
+ </ul>
14158
+ </div>
14159
+
14160
+ <ng-template #optionText let-parts="parts">
14161
+ <span class="lov-options-text">
14162
+ @for (part of parts; track $index) {
14163
+ @if (part.match) {
14164
+ <mark class="lov-highlight" [textContent]="part.text"></mark>
14165
+ } @else {
14166
+ <span [textContent]="part.text"></span>
14167
+ }
14168
+ }
14169
+ </span>
14170
+ </ng-template>
14171
+ `,
13882
14172
  encapsulation: ViewEncapsulation.None,
13883
14173
  standalone: false
13884
14174
  }]
@@ -13937,6 +14227,7 @@ class ListOfValuesComponent extends BaseInputComponent {
13937
14227
  ngZoneWrapper;
13938
14228
  elementRef;
13939
14229
  icons = CoreComponentsIcon;
14230
+ lovUtils = LovUtils;
13940
14231
  set model(value) {
13941
14232
  super.model = value;
13942
14233
  this._setSelectedModel();
@@ -13959,7 +14250,7 @@ class ListOfValuesComponent extends BaseInputComponent {
13959
14250
  collectionLoadFn;
13960
14251
  collectionLoadFnProp;
13961
14252
  leftIconData;
13962
- searchPlaceholder;
14253
+ searchPlaceholder = '';
13963
14254
  searchDisabled = false;
13964
14255
  showChips = true;
13965
14256
  showClass() {
@@ -13974,6 +14265,7 @@ class ListOfValuesComponent extends BaseInputComponent {
13974
14265
  isLoading = false;
13975
14266
  _collection = [];
13976
14267
  debounceTimeout;
14268
+ _filterRequestId = 0;
13977
14269
  _lovPopupComponentRef;
13978
14270
  constructor(formComponent, iconCacheService, changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef) {
13979
14271
  super(changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef);
@@ -13990,6 +14282,25 @@ class ListOfValuesComponent extends BaseInputComponent {
13990
14282
  super.ngOnInit();
13991
14283
  this._setSelectedModel();
13992
14284
  }
14285
+ // Value shown in the input: the search text while fetching (largeCollection), otherwise the
14286
+ // selected model(s). Lets a single <co-input-text> serve both modes without a template split.
14287
+ get inputModel() {
14288
+ if (this.largeCollection) {
14289
+ return this.filterText;
14290
+ }
14291
+ if (this.multiselect) {
14292
+ return (!this.selectedModels || this.selectedModels.length === 0) ? null : this.selectedModels;
14293
+ }
14294
+ return this.selectedModel;
14295
+ }
14296
+ handleModelChange(model) {
14297
+ if (this.largeCollection) {
14298
+ this.onModelChange(model);
14299
+ }
14300
+ else {
14301
+ this.handleInputModelChange(model);
14302
+ }
14303
+ }
13993
14304
  handleInputModelChange(model) {
13994
14305
  if (this._lovPopupComponentRef) {
13995
14306
  this._lovPopupComponentRef.instance.searchTerm = model;
@@ -14004,26 +14315,33 @@ class ListOfValuesComponent extends BaseInputComponent {
14004
14315
  }
14005
14316
  onModelChange(model) {
14006
14317
  this.isLoading = true;
14318
+ // Mark this keystroke as the latest request immediately, so an older in-flight
14319
+ // request that resolves within the debounce window is still discarded.
14320
+ const requestId = ++this._filterRequestId;
14007
14321
  clearTimeout(this.debounceTimeout);
14008
14322
  this.debounceTimeout = setTimeout(() => {
14009
- this.applyFilter(model);
14323
+ this.applyFilter(model, requestId);
14010
14324
  }, 300);
14011
14325
  }
14012
- async applyFilter(text) {
14326
+ async applyFilter(text, requestId = ++this._filterRequestId) {
14327
+ await new Promise(resolve => setTimeout(resolve, 300));
14328
+ if (requestId !== this._filterRequestId) {
14329
+ return;
14330
+ }
14013
14331
  if (text?.length < 3) {
14014
- await new Promise(resolve => setTimeout(resolve, 300));
14015
14332
  this.collection = undefined;
14333
+ this.filterText = text;
14334
+ this.closePopup();
14335
+ this.isLoading = false;
14336
+ this.changeDetector.detectChanges();
14337
+ return;
14016
14338
  }
14017
- else {
14018
- await new Promise(resolve => setTimeout(resolve, 300));
14019
- this.collection = await this.collectionLoadFn(text);
14339
+ const loaded = await this.collectionLoadFn(text);
14340
+ if (requestId !== this._filterRequestId) {
14341
+ return;
14020
14342
  }
14343
+ this.collection = loaded;
14021
14344
  this.filterText = text;
14022
- if (!this.collection) {
14023
- this.changeDetector.detectChanges();
14024
- this.isLoading = false;
14025
- return [];
14026
- }
14027
14345
  this.filteredCollection = this.collection?.filter((item) => {
14028
14346
  if (this.collectionLoadFnProp && this.collectionLoadFnProp.length > 0) {
14029
14347
  return item[this.collectionLoadFnProp] && item[this.collectionLoadFnProp].toLowerCase().includes(text.toLowerCase());
@@ -14032,12 +14350,19 @@ class ListOfValuesComponent extends BaseInputComponent {
14032
14350
  return true;
14033
14351
  }
14034
14352
  });
14035
- (this.filteredCollection?.length > 0 && this.filterText?.length > 2) ? this.openPopup() : this.closePopup();
14353
+ if (this.filteredCollection?.length > 0 && this.filterText?.length > 2) {
14354
+ this.openPopup(); // no-op when already open
14355
+ if (this._lovPopupComponentRef) {
14356
+ // Refresh the open popup in place; no re-create, so no flicker.
14357
+ this._lovPopupComponentRef.instance.collection = this.collection;
14358
+ this._lovPopupComponentRef.instance.searchTerm = text;
14359
+ }
14360
+ }
14361
+ else {
14362
+ this.closePopup();
14363
+ }
14036
14364
  this.isLoading = false;
14037
14365
  this.changeDetector.detectChanges();
14038
- if (this._lovPopupComponentRef) {
14039
- this._lovPopupComponentRef.instance.searchTerm = text;
14040
- }
14041
14366
  }
14042
14367
  handleInputKeyDown(event) {
14043
14368
  if (event) {
@@ -14082,6 +14407,11 @@ class ListOfValuesComponent extends BaseInputComponent {
14082
14407
  if (this.readonly) {
14083
14408
  return;
14084
14409
  }
14410
+ // Idempotent: keep the existing popup so continued typing refreshes it in place
14411
+ // instead of re-creating it (which would replay the open animation / flicker).
14412
+ if (this._lovPopupComponentRef) {
14413
+ return;
14414
+ }
14085
14415
  this.isSelectOpen = true;
14086
14416
  this._lovPopupComponentRef = this.overlayService.createComponent(ListOfValuesPopupComponent, {
14087
14417
  parentForOverlay: this.elementRef,
@@ -14111,12 +14441,12 @@ class ListOfValuesComponent extends BaseInputComponent {
14111
14441
  optionChosen(option) {
14112
14442
  if (option) {
14113
14443
  if (this.multiselect) {
14114
- this.selectedModels = option.map(o => o[this.displayField]);
14444
+ this.selectedModels = option.map(o => LovUtils.getDisplayValue(o, this.displayField));
14115
14445
  }
14116
14446
  else {
14117
- this.selectedModel = option[this.displayField];
14447
+ this.selectedModel = LovUtils.getDisplayValue(option, this.displayField);
14118
14448
  if (this.largeCollection) {
14119
- this.filterText = option[this.displayField];
14449
+ this.filterText = LovUtils.getDisplayValue(option, this.displayField);
14120
14450
  }
14121
14451
  }
14122
14452
  }
@@ -14140,8 +14470,13 @@ class ListOfValuesComponent extends BaseInputComponent {
14140
14470
  this.focused = false;
14141
14471
  }
14142
14472
  checkModel() {
14473
+ if (this.largeCollection) {
14474
+ // The input holds the search text (not a to-be-resolved value) and selection happens via the
14475
+ // popup, so blur must not try to match/clear the model here.
14476
+ return;
14477
+ }
14143
14478
  if (!this.multiselect && this.selectedModel && this.collection) {
14144
- const model = this.collection.find(c => c[this.displayField] === this.selectedModel);
14479
+ const model = this.collection.find(c => LovUtils.getDisplayValue(c, this.displayField) === this.selectedModel);
14145
14480
  if (model) {
14146
14481
  this.model = model;
14147
14482
  }
@@ -14156,7 +14491,7 @@ class ListOfValuesComponent extends BaseInputComponent {
14156
14491
  if (this.model) {
14157
14492
  this.selectedModels.length = 0;
14158
14493
  this.model.forEach(m => {
14159
- this.selectedModels.push(m[this.displayField]);
14494
+ this.selectedModels.push(LovUtils.getDisplayValue(m, this.displayField));
14160
14495
  });
14161
14496
  }
14162
14497
  else {
@@ -14165,13 +14500,14 @@ class ListOfValuesComponent extends BaseInputComponent {
14165
14500
  }
14166
14501
  else {
14167
14502
  if (this.model) {
14168
- this.selectedModel = this.model[this.displayField];
14503
+ this.selectedModel = LovUtils.getDisplayValue(this.model, this.displayField);
14169
14504
  if (this.largeCollection) {
14170
- this.filterText = this.model[this.displayField];
14505
+ this.filterText = LovUtils.getDisplayValue(this.model, this.displayField);
14171
14506
  }
14172
14507
  }
14173
14508
  else {
14174
14509
  this.selectedModel = '';
14510
+ this.filterText = '';
14175
14511
  }
14176
14512
  }
14177
14513
  }
@@ -14183,59 +14519,47 @@ class ListOfValuesComponent extends BaseInputComponent {
14183
14519
  useExisting: forwardRef(() => ListOfValuesComponent)
14184
14520
  }
14185
14521
  ], viewQueries: [{ propertyName: "parentForOverlay", first: true, predicate: ["parentForOverlay"], descendants: true, read: ElementRef }], usesInheritance: true, ngImport: i0, template: `
14186
- @if (!largeCollection) {
14187
- <co-input-text
14188
- aria-haspopup="listbox"
14189
- [attr.aria-expanded]="isSelectOpen"
14190
- aria-controls="lov-popup"
14191
- role="combobox"
14192
- class="no-focus-line"
14193
- overlayParent
14194
- #parentForOverlay="overlayParent"
14195
- type="text"
14196
- [id]="label"
14197
- [model]="multiselect ? ((!selectedModels || selectedModels.length === 0) ? null : selectedModels ) : selectedModel"
14198
- [placeholder]="label"
14199
- [myFormInputInstance]="this"
14200
- [readonly]="readonly"
14201
- [disabled]="disabled"
14202
- [required]="required"
14203
- [noClickFocus]="false"
14204
- [leftIconData]=" model && model[optionIcon] ? iconCacheService.getIcon(model[optionIcon]) : leftIconData"
14205
- [rightIcon]="isSelectOpen ? icons.ChevronUpRegular : icons.ChevronDownRegular"
14206
- [showClearButton]="true"
14207
- [useContent]="multiselect"
14208
- [customHeight]="multiselect"
14209
- [keepFocussed]="keepFocussed"
14210
- (modelChange)="handleInputModelChange($event)"
14211
- (click)="openPopup()"
14212
- (rightIconClick)="toggleSelect()"
14213
- (keydown)="handleInputKeyDown($event)"
14214
- (clearIconClick)="clearModel($event)"
14215
- (blur)="checkModel()">
14216
- @if (multiselect && showChips) {
14217
- <div class="multiselect-chips-wrapper">
14218
- @for (chip of model; track chip) {
14219
- <div class="chips">
14220
- <span class="chips-description" [textContent]="chip[displayField]"></span>
14522
+ <co-input-text
14523
+ aria-haspopup="listbox"
14524
+ [attr.aria-expanded]="isSelectOpen"
14525
+ aria-controls="lov-popup"
14526
+ role="combobox"
14527
+ class="no-focus-line"
14528
+ overlayParent
14529
+ #parentForOverlay="overlayParent"
14530
+ type="text"
14531
+ [id]="label"
14532
+ [model]="inputModel"
14533
+ [label]="label"
14534
+ [placeholder]="searchPlaceholder"
14535
+ [myFormInputInstance]="this"
14536
+ [readonly]="readonly"
14537
+ [disabled]="disabled"
14538
+ [required]="required"
14539
+ [noClickFocus]="false"
14540
+ [leftIconData]=" model && model[optionIcon] ? iconCacheService.getIcon(model[optionIcon]) : leftIconData"
14541
+ [rightIcon]="isSelectOpen ? icons.ChevronUpRegular : icons.ChevronDownRegular"
14542
+ [showClearButton]="true"
14543
+ [useContent]="multiselect"
14544
+ [customHeight]="multiselect"
14545
+ [keepFocussed]="keepFocussed"
14546
+ (modelChange)="handleModelChange($event)"
14547
+ (click)="openPopup()"
14548
+ (rightIconClick)="toggleSelect()"
14549
+ (keydown)="handleInputKeyDown($event)"
14550
+ (clearIconClick)="clearModel($event)"
14551
+ (blur)="checkModel()">
14552
+ @if (multiselect && showChips) {
14553
+ <div class="multiselect-chips-wrapper">
14554
+ @for (chip of model; track chip) {
14555
+ <div class="chips">
14556
+ <span class="chips-description" [textContent]="lovUtils.getDisplayValue(chip, displayField)"></span>
14221
14557
  <co-icon class="remove-chip-icon" [icon]="icons.CrossSkinny" (click)="removeOptionFromModel(chip)"></co-icon>
14222
- </div>
14223
- }
14224
- </div>
14225
- }
14226
- </co-input-text>
14227
- }
14228
-
14229
- @if (largeCollection) {
14230
- <co-input-text
14231
- [model]="filterText"
14232
- [placeholder]="label"
14233
- [required]="required"
14234
- [disabled]="disabled"
14235
- [readonly]="readonly"
14236
- (modelChange)="onModelChange($event)">
14237
- </co-input-text>
14238
- }
14558
+ </div>
14559
+ }
14560
+ </div>
14561
+ }
14562
+ </co-input-text>
14239
14563
  @if (isLoading) {
14240
14564
  <div class="filter-loader"><span></span></div>
14241
14565
  }
@@ -14247,59 +14571,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
14247
14571
  args: [{
14248
14572
  selector: 'co-list-of-values',
14249
14573
  template: `
14250
- @if (!largeCollection) {
14251
- <co-input-text
14252
- aria-haspopup="listbox"
14253
- [attr.aria-expanded]="isSelectOpen"
14254
- aria-controls="lov-popup"
14255
- role="combobox"
14256
- class="no-focus-line"
14257
- overlayParent
14258
- #parentForOverlay="overlayParent"
14259
- type="text"
14260
- [id]="label"
14261
- [model]="multiselect ? ((!selectedModels || selectedModels.length === 0) ? null : selectedModels ) : selectedModel"
14262
- [placeholder]="label"
14263
- [myFormInputInstance]="this"
14264
- [readonly]="readonly"
14265
- [disabled]="disabled"
14266
- [required]="required"
14267
- [noClickFocus]="false"
14268
- [leftIconData]=" model && model[optionIcon] ? iconCacheService.getIcon(model[optionIcon]) : leftIconData"
14269
- [rightIcon]="isSelectOpen ? icons.ChevronUpRegular : icons.ChevronDownRegular"
14270
- [showClearButton]="true"
14271
- [useContent]="multiselect"
14272
- [customHeight]="multiselect"
14273
- [keepFocussed]="keepFocussed"
14274
- (modelChange)="handleInputModelChange($event)"
14275
- (click)="openPopup()"
14276
- (rightIconClick)="toggleSelect()"
14277
- (keydown)="handleInputKeyDown($event)"
14278
- (clearIconClick)="clearModel($event)"
14279
- (blur)="checkModel()">
14280
- @if (multiselect && showChips) {
14281
- <div class="multiselect-chips-wrapper">
14282
- @for (chip of model; track chip) {
14283
- <div class="chips">
14284
- <span class="chips-description" [textContent]="chip[displayField]"></span>
14574
+ <co-input-text
14575
+ aria-haspopup="listbox"
14576
+ [attr.aria-expanded]="isSelectOpen"
14577
+ aria-controls="lov-popup"
14578
+ role="combobox"
14579
+ class="no-focus-line"
14580
+ overlayParent
14581
+ #parentForOverlay="overlayParent"
14582
+ type="text"
14583
+ [id]="label"
14584
+ [model]="inputModel"
14585
+ [label]="label"
14586
+ [placeholder]="searchPlaceholder"
14587
+ [myFormInputInstance]="this"
14588
+ [readonly]="readonly"
14589
+ [disabled]="disabled"
14590
+ [required]="required"
14591
+ [noClickFocus]="false"
14592
+ [leftIconData]=" model && model[optionIcon] ? iconCacheService.getIcon(model[optionIcon]) : leftIconData"
14593
+ [rightIcon]="isSelectOpen ? icons.ChevronUpRegular : icons.ChevronDownRegular"
14594
+ [showClearButton]="true"
14595
+ [useContent]="multiselect"
14596
+ [customHeight]="multiselect"
14597
+ [keepFocussed]="keepFocussed"
14598
+ (modelChange)="handleModelChange($event)"
14599
+ (click)="openPopup()"
14600
+ (rightIconClick)="toggleSelect()"
14601
+ (keydown)="handleInputKeyDown($event)"
14602
+ (clearIconClick)="clearModel($event)"
14603
+ (blur)="checkModel()">
14604
+ @if (multiselect && showChips) {
14605
+ <div class="multiselect-chips-wrapper">
14606
+ @for (chip of model; track chip) {
14607
+ <div class="chips">
14608
+ <span class="chips-description" [textContent]="lovUtils.getDisplayValue(chip, displayField)"></span>
14285
14609
  <co-icon class="remove-chip-icon" [icon]="icons.CrossSkinny" (click)="removeOptionFromModel(chip)"></co-icon>
14286
- </div>
14287
- }
14288
- </div>
14289
- }
14290
- </co-input-text>
14291
- }
14292
-
14293
- @if (largeCollection) {
14294
- <co-input-text
14295
- [model]="filterText"
14296
- [placeholder]="label"
14297
- [required]="required"
14298
- [disabled]="disabled"
14299
- [readonly]="readonly"
14300
- (modelChange)="onModelChange($event)">
14301
- </co-input-text>
14302
- }
14610
+ </div>
14611
+ }
14612
+ </div>
14613
+ }
14614
+ </co-input-text>
14303
14615
  @if (isLoading) {
14304
14616
  <div class="filter-loader"><span></span></div>
14305
14617
  }
@@ -16556,9 +16868,7 @@ class FilterItemModule {
16556
16868
  ScrollingModule,
16557
16869
  AppendPipeModule,
16558
16870
  PrependPipeModule,
16559
- InputTextModule,
16560
16871
  InputRadioButtonModule,
16561
- InputTextModule,
16562
16872
  CoreComponentsTranslationModule,
16563
16873
  InputDatePickerModule,
16564
16874
  InputDateRangePickerModule], exports: [FilterItemComponent] });
@@ -16570,9 +16880,7 @@ class FilterItemModule {
16570
16880
  ScrollingModule,
16571
16881
  AppendPipeModule,
16572
16882
  PrependPipeModule,
16573
- InputTextModule,
16574
16883
  InputRadioButtonModule,
16575
- InputTextModule,
16576
16884
  CoreComponentsTranslationModule,
16577
16885
  InputDatePickerModule,
16578
16886
  InputDateRangePickerModule] });
@@ -16589,9 +16897,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
16589
16897
  ScrollingModule,
16590
16898
  AppendPipeModule,
16591
16899
  PrependPipeModule,
16592
- InputTextModule,
16593
16900
  InputRadioButtonModule,
16594
- InputTextModule,
16595
16901
  CoreComponentsTranslationModule,
16596
16902
  InputDatePickerModule,
16597
16903
  InputDateRangePickerModule