@colijnit/corecomponents_v12 261.20.17 → 261.20.19

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) => {
@@ -9148,6 +9337,9 @@ class InputNumberPickerComponent extends BaseInputComponent {
9148
9337
  }
9149
9338
  handleInputKeyDown(event) {
9150
9339
  // event.preventDefault();
9340
+ if (this.readonly) {
9341
+ return false;
9342
+ }
9151
9343
  if (event.code === 'Enter' || event.code === 'NumpadEnter') {
9152
9344
  this.modelChange.next(this.model);
9153
9345
  return false;
@@ -9155,13 +9347,40 @@ class InputNumberPickerComponent extends BaseInputComponent {
9155
9347
  if (this.myKeyDownWhiteList.find((k) => k === event.keyCode) === undefined) {
9156
9348
  return false;
9157
9349
  }
9350
+ const currentText = this.input ? this.input.value : String(this.model ?? '');
9351
+ if (event.key === '.' || event.key === ',') {
9352
+ // only one decimal separator, and only the locale one: normalize a foreign '.'/',' to it
9353
+ if (/[.,]/.test(currentText)) {
9354
+ return false;
9355
+ }
9356
+ if (event.key !== this._numberLocale.decimalSeparator) {
9357
+ if (this.input) {
9358
+ this._numberLocale.insertDecimalSeparator(this.input);
9359
+ }
9360
+ return false;
9361
+ }
9362
+ }
9363
+ // native text/number inputs ignore maxlength, so enforce maxLength (precision) and decimals (scale) here
9364
+ if (/^\d$/.test(event.key) && NumberUtils.WouldExceedDigitLimits(currentText, this.maxLength, this.decimals)) {
9365
+ return false;
9366
+ }
9367
+ }
9368
+ // precision/scale is enforced in keydown; the native maxlength on this text input would wrongly count
9369
+ // the decimal separator as a character (e.g. blocking "1234567,89" at maxLength 9), so don't set it.
9370
+ _setNativeMaxLength() {
9158
9371
  }
9159
9372
  handleBlur() {
9373
+ // normalize the display once editing stops (e.g. "12," -> "12", trailing zeros dropped)
9374
+ this._displayValue = this._numberLocale.format(this.model);
9160
9375
  this.modelChange.next(this.model);
9161
9376
  }
9162
- handleChangeModel(value) {
9377
+ handleChangeModel(text) {
9163
9378
  this._changeFromButton = false;
9164
- this.numberLogic.setValue(value);
9379
+ // preserve exactly what the user typed while they are typing (avoids caret jumps / eating the separator)
9380
+ this._userTyping = true;
9381
+ this._displayValue = text === null || text === undefined ? '' : '' + text;
9382
+ this.numberLogic.setValue(this._numberLocale.parse(text));
9383
+ this._userTyping = false;
9165
9384
  }
9166
9385
  // Note: recursive through setTimeout().
9167
9386
  doDecrementAuto() {
@@ -9263,7 +9482,7 @@ class InputNumberPickerComponent extends BaseInputComponent {
9263
9482
  break;
9264
9483
  }
9265
9484
  }
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 });
9485
+ 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
9486
  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
9487
  OverlayService, {
9269
9488
  provide: SCREEN_CONFIG_ADAPTER_COMPONENT_INTERFACE_NAME, useExisting: forwardRef(() => InputNumberPickerComponent)
@@ -9289,15 +9508,15 @@ class InputNumberPickerComponent extends BaseInputComponent {
9289
9508
  </div>
9290
9509
  <div class="input-wrapper">
9291
9510
  @if (showPermanentLabel) {
9292
- <span class='permanent-label' [textContent]="label"></span>
9511
+ <span class='permanent-label' [textContent]="label ?? ''"></span>
9293
9512
  }
9294
- <input type="number"
9513
+ <input type="text" inputmode="decimal"
9295
9514
  [tabIndex]="readonly ? -1 : 0"
9296
- [ngModel]="model"
9515
+ [ngModel]="displayValue"
9297
9516
  [readonly]="readonly"
9298
9517
  [disabled]="disabled"
9299
9518
  [required]="required"
9300
- [placeholder]="label"
9519
+ [placeholder]="label ?? ''"
9301
9520
  (ngModelChange)="handleChangeModel($event)"
9302
9521
  (keydown)="handleInputKeyDown($event)"
9303
9522
  (blur)="handleBlur()"/>
@@ -9311,7 +9530,7 @@ class InputNumberPickerComponent extends BaseInputComponent {
9311
9530
  (mouseup)="stopAutoCounting()" (mouseleave)="stopAutoCounting()"></co-button>
9312
9531
  }
9313
9532
  </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 });
9533
+ `, 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
9534
  }
9316
9535
  __decorate([
9317
9536
  InputBoolean()
@@ -9338,15 +9557,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9338
9557
  </div>
9339
9558
  <div class="input-wrapper">
9340
9559
  @if (showPermanentLabel) {
9341
- <span class='permanent-label' [textContent]="label"></span>
9560
+ <span class='permanent-label' [textContent]="label ?? ''"></span>
9342
9561
  }
9343
- <input type="number"
9562
+ <input type="text" inputmode="decimal"
9344
9563
  [tabIndex]="readonly ? -1 : 0"
9345
- [ngModel]="model"
9564
+ [ngModel]="displayValue"
9346
9565
  [readonly]="readonly"
9347
9566
  [disabled]="disabled"
9348
9567
  [required]="required"
9349
- [placeholder]="label"
9568
+ [placeholder]="label ?? ''"
9350
9569
  (ngModelChange)="handleChangeModel($event)"
9351
9570
  (keydown)="handleInputKeyDown($event)"
9352
9571
  (blur)="handleBlur()"/>
@@ -9378,7 +9597,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9378
9597
  }] }, { type: IconCacheService, decorators: [{
9379
9598
  type: Inject,
9380
9599
  args: [IconCacheService]
9381
- }] }, { type: OverlayService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: FormInputUserModelChangeListenerService }, { type: NgZoneWrapperService }, { type: i0.ElementRef }], propDecorators: { model: [{
9600
+ }] }, { type: OverlayService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: FormInputUserModelChangeListenerService }, { type: NumberLocaleService }, { type: NgZoneWrapperService }, { type: i0.ElementRef }], propDecorators: { model: [{
9382
9601
  type: Input
9383
9602
  }], modelChangeOnEnter: [{
9384
9603
  type: Input
@@ -13527,6 +13746,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
13527
13746
  }]
13528
13747
  }] });
13529
13748
 
13749
+ class LovUtils {
13750
+ static getDisplayValue(model, displayField) {
13751
+ if (!model || !displayField) {
13752
+ return '';
13753
+ }
13754
+ return displayField
13755
+ .split('|')
13756
+ .map(field => model[field.trim()])
13757
+ .filter(val => val != null && val !== '')
13758
+ .join(' - ');
13759
+ }
13760
+ static getHighlightParts(value, term) {
13761
+ const text = value ?? '';
13762
+ if (!text || !term) {
13763
+ return [{ text: text, match: false }];
13764
+ }
13765
+ const regex = new RegExp(LovUtils._escapeRegExp(term), 'gi');
13766
+ const parts = [];
13767
+ let lastIndex = 0;
13768
+ let match;
13769
+ while ((match = regex.exec(text)) !== null) {
13770
+ if (match.index > lastIndex) {
13771
+ parts.push({ text: text.slice(lastIndex, match.index), match: false });
13772
+ }
13773
+ parts.push({ text: match[0], match: true });
13774
+ lastIndex = match.index + match[0].length;
13775
+ if (match.index === regex.lastIndex) {
13776
+ regex.lastIndex++;
13777
+ }
13778
+ }
13779
+ if (lastIndex < text.length) {
13780
+ parts.push({ text: text.slice(lastIndex), match: false });
13781
+ }
13782
+ return parts;
13783
+ }
13784
+ static _escapeRegExp(value) {
13785
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13786
+ }
13787
+ }
13788
+
13530
13789
  class ListOfValuesPopupComponent {
13531
13790
  iconCacheService;
13532
13791
  _elementRef;
@@ -13647,7 +13906,11 @@ class ListOfValuesPopupComponent {
13647
13906
  }
13648
13907
  }
13649
13908
  selectModelByViewModel(viewModel, closePopup = true) {
13650
- this.model = this.viewModelsMain.find(vm => vm === viewModel).model;
13909
+ const found = this.viewModelsMain.find(vm => vm === viewModel);
13910
+ if (!found) {
13911
+ return;
13912
+ }
13913
+ this.model = found.model;
13651
13914
  this.modelChange.emit(this.model);
13652
13915
  this._scrollIntoView();
13653
13916
  if (closePopup) {
@@ -13660,7 +13923,7 @@ class ListOfValuesPopupComponent {
13660
13923
  this.selectNextOption();
13661
13924
  }
13662
13925
  if (!this.model && this.searchTerm) {
13663
- const wishModel = this.viewModelsMain.find(vmm => vmm.model[this.displayField] === this.searchTerm);
13926
+ const wishModel = this.viewModelsMain.find(vmm => LovUtils.getDisplayValue(vmm.model, this.displayField) === this.searchTerm);
13664
13927
  if (wishModel) {
13665
13928
  this.selectViewModel(wishModel);
13666
13929
  }
@@ -13680,6 +13943,9 @@ class ListOfValuesPopupComponent {
13680
13943
  this.modelChange.emit(this.model);
13681
13944
  }
13682
13945
  selectNextOption(back = false) {
13946
+ if (this.viewModels.length === 0) {
13947
+ return;
13948
+ }
13683
13949
  let nextModel;
13684
13950
  if (!this.highLightModel) {
13685
13951
  nextModel = this.viewModels[back ? this.viewModels.length - 1 : 0];
@@ -13733,8 +13999,8 @@ class ListOfValuesPopupComponent {
13733
13999
  }
13734
14000
  if (!this.model)
13735
14001
  return false;
13736
- const selected = this.model?.[this.displayField];
13737
- const current = vm.model?.[this.displayField];
14002
+ const selected = LovUtils.getDisplayValue(this.model, this.displayField);
14003
+ const current = LovUtils.getDisplayValue(vm.model, this.displayField);
13738
14004
  return selected != null && current != null && selected === current;
13739
14005
  }
13740
14006
  _prepareViewModelsMain() {
@@ -13755,6 +14021,9 @@ class ListOfValuesPopupComponent {
13755
14021
  }
13756
14022
  });
13757
14023
  });
14024
+ this.viewModels.forEach((vm) => {
14025
+ vm.highlightParts = LovUtils.getHighlightParts(LovUtils.getDisplayValue(vm.model, this.displayField), this.searchTerm);
14026
+ });
13758
14027
  }
13759
14028
  _scrollIntoView() {
13760
14029
  const activeIndex = this.viewModels.findIndex(vmm => vmm === this.highLightModel);
@@ -13783,102 +14052,126 @@ class ListOfValuesPopupComponent {
13783
14052
  }
13784
14053
  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
14054
  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>
14055
+ <div class="lov-options" [overlay]="parentForOverlay" [inheritWidth]="true" [ngClass]="customCssClass"
14056
+ id="lov-popup"
14057
+ role="listbox" [tabindex]="-1"
14058
+ (clickOutside)="closePopup.emit($event)">
14059
+ @if (multiselect) {
14060
+ <co-input-search
14061
+ tabindex="-1"
14062
+ [(model)]="searchTerm"
14063
+ [placeholder]="searchPlaceholder"
14064
+ (keydown)="handleInputKeyDown($event)"
14065
+ (modelChange)="filterViewModels()"></co-input-search>
14066
+ }
14067
+ @if (showToggleAll && multiselect) {
14068
+ <div class="row gap">
14069
+ <co-input-checkbox [model]="allSelected" (modelChange)="toggleAll()"></co-input-checkbox>
14070
+ <span [textContent]="'DESELECT_ALL' | coreLocalize" (click)="toggleAll()"></span>
14071
+ </div>
14072
+ }
14073
+ <ul class="dropdown-list" #dropDownList>
14074
+ @for (viewModel of viewModels; track viewModel; let index = $index) {
14075
+ <li
14076
+ #lovItem
14077
+ [class.selected]="viewModel === highLightModel || viewModels.length === 1"
14078
+ [class.active]="isActive(viewModel)"
14079
+ [class.highlighted]="viewModel === highLightModel"
14080
+ [attr.aria-selected]="isActive(viewModel)"
14081
+ (click)="selectViewModel(viewModel, !multiselect)"
14082
+ role="option">
14083
+ @if (!multiselect) {
14084
+ @if (viewModel.model[optionIcon]) {
14085
+ <co-icon class="input-text-left-icon" [iconData]="iconCacheService.getIcon(viewModel.model[optionIcon])">
14086
+ </co-icon>
14087
+ }
14088
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14089
+ }
14090
+ @if (multiselect) {
14091
+ <co-input-checkbox [model]="viewModel.checked"
14092
+ (modelChange)="selectViewModel(viewModel, false)"></co-input-checkbox>
14093
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14094
+ }
14095
+ </li>
13818
14096
  }
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 });
14097
+ </ul>
14098
+ </div>
14099
+
14100
+ <ng-template #optionText let-parts="parts">
14101
+ <span class="lov-options-text">
14102
+ @for (part of parts; track $index) {
14103
+ @if (part.match) {
14104
+ <mark class="lov-highlight" [textContent]="part.text"></mark>
14105
+ } @else {
14106
+ <span [textContent]="part.text"></span>
14107
+ }
14108
+ }
14109
+ </span>
14110
+ </ng-template>
14111
+ `, 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
14112
  }
13832
14113
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ListOfValuesPopupComponent, decorators: [{
13833
14114
  type: Component,
13834
14115
  args: [{
13835
14116
  selector: 'co-list-of-values-popup',
13836
14117
  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>
14118
+ <div class="lov-options" [overlay]="parentForOverlay" [inheritWidth]="true" [ngClass]="customCssClass"
14119
+ id="lov-popup"
14120
+ role="listbox" [tabindex]="-1"
14121
+ (clickOutside)="closePopup.emit($event)">
14122
+ @if (multiselect) {
14123
+ <co-input-search
14124
+ tabindex="-1"
14125
+ [(model)]="searchTerm"
14126
+ [placeholder]="searchPlaceholder"
14127
+ (keydown)="handleInputKeyDown($event)"
14128
+ (modelChange)="filterViewModels()"></co-input-search>
14129
+ }
14130
+ @if (showToggleAll && multiselect) {
14131
+ <div class="row gap">
14132
+ <co-input-checkbox [model]="allSelected" (modelChange)="toggleAll()"></co-input-checkbox>
14133
+ <span [textContent]="'DESELECT_ALL' | coreLocalize" (click)="toggleAll()"></span>
14134
+ </div>
14135
+ }
14136
+ <ul class="dropdown-list" #dropDownList>
14137
+ @for (viewModel of viewModels; track viewModel; let index = $index) {
14138
+ <li
14139
+ #lovItem
14140
+ [class.selected]="viewModel === highLightModel || viewModels.length === 1"
14141
+ [class.active]="isActive(viewModel)"
14142
+ [class.highlighted]="viewModel === highLightModel"
14143
+ [attr.aria-selected]="isActive(viewModel)"
14144
+ (click)="selectViewModel(viewModel, !multiselect)"
14145
+ role="option">
14146
+ @if (!multiselect) {
14147
+ @if (viewModel.model[optionIcon]) {
14148
+ <co-icon class="input-text-left-icon" [iconData]="iconCacheService.getIcon(viewModel.model[optionIcon])">
14149
+ </co-icon>
14150
+ }
14151
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14152
+ }
14153
+ @if (multiselect) {
14154
+ <co-input-checkbox [model]="viewModel.checked"
14155
+ (modelChange)="selectViewModel(viewModel, false)"></co-input-checkbox>
14156
+ <ng-container [ngTemplateOutlet]="optionText" [ngTemplateOutletContext]="{parts: viewModel.highlightParts}"></ng-container>
14157
+ }
14158
+ </li>
13869
14159
  }
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
- `,
14160
+ </ul>
14161
+ </div>
14162
+
14163
+ <ng-template #optionText let-parts="parts">
14164
+ <span class="lov-options-text">
14165
+ @for (part of parts; track $index) {
14166
+ @if (part.match) {
14167
+ <mark class="lov-highlight" [textContent]="part.text"></mark>
14168
+ } @else {
14169
+ <span [textContent]="part.text"></span>
14170
+ }
14171
+ }
14172
+ </span>
14173
+ </ng-template>
14174
+ `,
13882
14175
  encapsulation: ViewEncapsulation.None,
13883
14176
  standalone: false
13884
14177
  }]
@@ -13937,6 +14230,7 @@ class ListOfValuesComponent extends BaseInputComponent {
13937
14230
  ngZoneWrapper;
13938
14231
  elementRef;
13939
14232
  icons = CoreComponentsIcon;
14233
+ lovUtils = LovUtils;
13940
14234
  set model(value) {
13941
14235
  super.model = value;
13942
14236
  this._setSelectedModel();
@@ -13959,7 +14253,7 @@ class ListOfValuesComponent extends BaseInputComponent {
13959
14253
  collectionLoadFn;
13960
14254
  collectionLoadFnProp;
13961
14255
  leftIconData;
13962
- searchPlaceholder;
14256
+ searchPlaceholder = '';
13963
14257
  searchDisabled = false;
13964
14258
  showChips = true;
13965
14259
  showClass() {
@@ -13974,6 +14268,7 @@ class ListOfValuesComponent extends BaseInputComponent {
13974
14268
  isLoading = false;
13975
14269
  _collection = [];
13976
14270
  debounceTimeout;
14271
+ _filterRequestId = 0;
13977
14272
  _lovPopupComponentRef;
13978
14273
  constructor(formComponent, iconCacheService, changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef) {
13979
14274
  super(changeDetector, overlayService, formUserChangeListener, ngZoneWrapper, elementRef);
@@ -13990,6 +14285,25 @@ class ListOfValuesComponent extends BaseInputComponent {
13990
14285
  super.ngOnInit();
13991
14286
  this._setSelectedModel();
13992
14287
  }
14288
+ // Value shown in the input: the search text while fetching (largeCollection), otherwise the
14289
+ // selected model(s). Lets a single <co-input-text> serve both modes without a template split.
14290
+ get inputModel() {
14291
+ if (this.largeCollection) {
14292
+ return this.filterText;
14293
+ }
14294
+ if (this.multiselect) {
14295
+ return (!this.selectedModels || this.selectedModels.length === 0) ? null : this.selectedModels;
14296
+ }
14297
+ return this.selectedModel;
14298
+ }
14299
+ handleModelChange(model) {
14300
+ if (this.largeCollection) {
14301
+ this.onModelChange(model);
14302
+ }
14303
+ else {
14304
+ this.handleInputModelChange(model);
14305
+ }
14306
+ }
13993
14307
  handleInputModelChange(model) {
13994
14308
  if (this._lovPopupComponentRef) {
13995
14309
  this._lovPopupComponentRef.instance.searchTerm = model;
@@ -14004,26 +14318,33 @@ class ListOfValuesComponent extends BaseInputComponent {
14004
14318
  }
14005
14319
  onModelChange(model) {
14006
14320
  this.isLoading = true;
14321
+ // Mark this keystroke as the latest request immediately, so an older in-flight
14322
+ // request that resolves within the debounce window is still discarded.
14323
+ const requestId = ++this._filterRequestId;
14007
14324
  clearTimeout(this.debounceTimeout);
14008
14325
  this.debounceTimeout = setTimeout(() => {
14009
- this.applyFilter(model);
14326
+ this.applyFilter(model, requestId);
14010
14327
  }, 300);
14011
14328
  }
14012
- async applyFilter(text) {
14329
+ async applyFilter(text, requestId = ++this._filterRequestId) {
14330
+ await new Promise(resolve => setTimeout(resolve, 300));
14331
+ if (requestId !== this._filterRequestId) {
14332
+ return;
14333
+ }
14013
14334
  if (text?.length < 3) {
14014
- await new Promise(resolve => setTimeout(resolve, 300));
14015
14335
  this.collection = undefined;
14336
+ this.filterText = text;
14337
+ this.closePopup();
14338
+ this.isLoading = false;
14339
+ this.changeDetector.detectChanges();
14340
+ return;
14016
14341
  }
14017
- else {
14018
- await new Promise(resolve => setTimeout(resolve, 300));
14019
- this.collection = await this.collectionLoadFn(text);
14342
+ const loaded = await this.collectionLoadFn(text);
14343
+ if (requestId !== this._filterRequestId) {
14344
+ return;
14020
14345
  }
14346
+ this.collection = loaded;
14021
14347
  this.filterText = text;
14022
- if (!this.collection) {
14023
- this.changeDetector.detectChanges();
14024
- this.isLoading = false;
14025
- return [];
14026
- }
14027
14348
  this.filteredCollection = this.collection?.filter((item) => {
14028
14349
  if (this.collectionLoadFnProp && this.collectionLoadFnProp.length > 0) {
14029
14350
  return item[this.collectionLoadFnProp] && item[this.collectionLoadFnProp].toLowerCase().includes(text.toLowerCase());
@@ -14032,12 +14353,19 @@ class ListOfValuesComponent extends BaseInputComponent {
14032
14353
  return true;
14033
14354
  }
14034
14355
  });
14035
- (this.filteredCollection?.length > 0 && this.filterText?.length > 2) ? this.openPopup() : this.closePopup();
14356
+ if (this.filteredCollection?.length > 0 && this.filterText?.length > 2) {
14357
+ this.openPopup(); // no-op when already open
14358
+ if (this._lovPopupComponentRef) {
14359
+ // Refresh the open popup in place; no re-create, so no flicker.
14360
+ this._lovPopupComponentRef.instance.collection = this.collection;
14361
+ this._lovPopupComponentRef.instance.searchTerm = text;
14362
+ }
14363
+ }
14364
+ else {
14365
+ this.closePopup();
14366
+ }
14036
14367
  this.isLoading = false;
14037
14368
  this.changeDetector.detectChanges();
14038
- if (this._lovPopupComponentRef) {
14039
- this._lovPopupComponentRef.instance.searchTerm = text;
14040
- }
14041
14369
  }
14042
14370
  handleInputKeyDown(event) {
14043
14371
  if (event) {
@@ -14082,6 +14410,11 @@ class ListOfValuesComponent extends BaseInputComponent {
14082
14410
  if (this.readonly) {
14083
14411
  return;
14084
14412
  }
14413
+ // Idempotent: keep the existing popup so continued typing refreshes it in place
14414
+ // instead of re-creating it (which would replay the open animation / flicker).
14415
+ if (this._lovPopupComponentRef) {
14416
+ return;
14417
+ }
14085
14418
  this.isSelectOpen = true;
14086
14419
  this._lovPopupComponentRef = this.overlayService.createComponent(ListOfValuesPopupComponent, {
14087
14420
  parentForOverlay: this.elementRef,
@@ -14111,12 +14444,12 @@ class ListOfValuesComponent extends BaseInputComponent {
14111
14444
  optionChosen(option) {
14112
14445
  if (option) {
14113
14446
  if (this.multiselect) {
14114
- this.selectedModels = option.map(o => o[this.displayField]);
14447
+ this.selectedModels = option.map(o => LovUtils.getDisplayValue(o, this.displayField));
14115
14448
  }
14116
14449
  else {
14117
- this.selectedModel = option[this.displayField];
14450
+ this.selectedModel = LovUtils.getDisplayValue(option, this.displayField);
14118
14451
  if (this.largeCollection) {
14119
- this.filterText = option[this.displayField];
14452
+ this.filterText = LovUtils.getDisplayValue(option, this.displayField);
14120
14453
  }
14121
14454
  }
14122
14455
  }
@@ -14140,8 +14473,13 @@ class ListOfValuesComponent extends BaseInputComponent {
14140
14473
  this.focused = false;
14141
14474
  }
14142
14475
  checkModel() {
14476
+ if (this.largeCollection) {
14477
+ // The input holds the search text (not a to-be-resolved value) and selection happens via the
14478
+ // popup, so blur must not try to match/clear the model here.
14479
+ return;
14480
+ }
14143
14481
  if (!this.multiselect && this.selectedModel && this.collection) {
14144
- const model = this.collection.find(c => c[this.displayField] === this.selectedModel);
14482
+ const model = this.collection.find(c => LovUtils.getDisplayValue(c, this.displayField) === this.selectedModel);
14145
14483
  if (model) {
14146
14484
  this.model = model;
14147
14485
  }
@@ -14156,7 +14494,7 @@ class ListOfValuesComponent extends BaseInputComponent {
14156
14494
  if (this.model) {
14157
14495
  this.selectedModels.length = 0;
14158
14496
  this.model.forEach(m => {
14159
- this.selectedModels.push(m[this.displayField]);
14497
+ this.selectedModels.push(LovUtils.getDisplayValue(m, this.displayField));
14160
14498
  });
14161
14499
  }
14162
14500
  else {
@@ -14165,13 +14503,14 @@ class ListOfValuesComponent extends BaseInputComponent {
14165
14503
  }
14166
14504
  else {
14167
14505
  if (this.model) {
14168
- this.selectedModel = this.model[this.displayField];
14506
+ this.selectedModel = LovUtils.getDisplayValue(this.model, this.displayField);
14169
14507
  if (this.largeCollection) {
14170
- this.filterText = this.model[this.displayField];
14508
+ this.filterText = LovUtils.getDisplayValue(this.model, this.displayField);
14171
14509
  }
14172
14510
  }
14173
14511
  else {
14174
14512
  this.selectedModel = '';
14513
+ this.filterText = '';
14175
14514
  }
14176
14515
  }
14177
14516
  }
@@ -14183,59 +14522,47 @@ class ListOfValuesComponent extends BaseInputComponent {
14183
14522
  useExisting: forwardRef(() => ListOfValuesComponent)
14184
14523
  }
14185
14524
  ], 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>
14525
+ <co-input-text
14526
+ aria-haspopup="listbox"
14527
+ [attr.aria-expanded]="isSelectOpen"
14528
+ aria-controls="lov-popup"
14529
+ role="combobox"
14530
+ class="no-focus-line"
14531
+ overlayParent
14532
+ #parentForOverlay="overlayParent"
14533
+ type="text"
14534
+ [id]="label"
14535
+ [model]="inputModel"
14536
+ [label]="label"
14537
+ [placeholder]="searchPlaceholder"
14538
+ [myFormInputInstance]="this"
14539
+ [readonly]="readonly"
14540
+ [disabled]="disabled"
14541
+ [required]="required"
14542
+ [noClickFocus]="false"
14543
+ [leftIconData]=" model && model[optionIcon] ? iconCacheService.getIcon(model[optionIcon]) : leftIconData"
14544
+ [rightIcon]="isSelectOpen ? icons.ChevronUpRegular : icons.ChevronDownRegular"
14545
+ [showClearButton]="true"
14546
+ [useContent]="multiselect"
14547
+ [customHeight]="multiselect"
14548
+ [keepFocussed]="keepFocussed"
14549
+ (modelChange)="handleModelChange($event)"
14550
+ (click)="openPopup()"
14551
+ (rightIconClick)="toggleSelect()"
14552
+ (keydown)="handleInputKeyDown($event)"
14553
+ (clearIconClick)="clearModel($event)"
14554
+ (blur)="checkModel()">
14555
+ @if (multiselect && showChips) {
14556
+ <div class="multiselect-chips-wrapper">
14557
+ @for (chip of model; track chip) {
14558
+ <div class="chips">
14559
+ <span class="chips-description" [textContent]="lovUtils.getDisplayValue(chip, displayField)"></span>
14221
14560
  <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
- }
14561
+ </div>
14562
+ }
14563
+ </div>
14564
+ }
14565
+ </co-input-text>
14239
14566
  @if (isLoading) {
14240
14567
  <div class="filter-loader"><span></span></div>
14241
14568
  }
@@ -14247,59 +14574,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
14247
14574
  args: [{
14248
14575
  selector: 'co-list-of-values',
14249
14576
  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>
14577
+ <co-input-text
14578
+ aria-haspopup="listbox"
14579
+ [attr.aria-expanded]="isSelectOpen"
14580
+ aria-controls="lov-popup"
14581
+ role="combobox"
14582
+ class="no-focus-line"
14583
+ overlayParent
14584
+ #parentForOverlay="overlayParent"
14585
+ type="text"
14586
+ [id]="label"
14587
+ [model]="inputModel"
14588
+ [label]="label"
14589
+ [placeholder]="searchPlaceholder"
14590
+ [myFormInputInstance]="this"
14591
+ [readonly]="readonly"
14592
+ [disabled]="disabled"
14593
+ [required]="required"
14594
+ [noClickFocus]="false"
14595
+ [leftIconData]=" model && model[optionIcon] ? iconCacheService.getIcon(model[optionIcon]) : leftIconData"
14596
+ [rightIcon]="isSelectOpen ? icons.ChevronUpRegular : icons.ChevronDownRegular"
14597
+ [showClearButton]="true"
14598
+ [useContent]="multiselect"
14599
+ [customHeight]="multiselect"
14600
+ [keepFocussed]="keepFocussed"
14601
+ (modelChange)="handleModelChange($event)"
14602
+ (click)="openPopup()"
14603
+ (rightIconClick)="toggleSelect()"
14604
+ (keydown)="handleInputKeyDown($event)"
14605
+ (clearIconClick)="clearModel($event)"
14606
+ (blur)="checkModel()">
14607
+ @if (multiselect && showChips) {
14608
+ <div class="multiselect-chips-wrapper">
14609
+ @for (chip of model; track chip) {
14610
+ <div class="chips">
14611
+ <span class="chips-description" [textContent]="lovUtils.getDisplayValue(chip, displayField)"></span>
14285
14612
  <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
- }
14613
+ </div>
14614
+ }
14615
+ </div>
14616
+ }
14617
+ </co-input-text>
14303
14618
  @if (isLoading) {
14304
14619
  <div class="filter-loader"><span></span></div>
14305
14620
  }
@@ -16556,9 +16871,7 @@ class FilterItemModule {
16556
16871
  ScrollingModule,
16557
16872
  AppendPipeModule,
16558
16873
  PrependPipeModule,
16559
- InputTextModule,
16560
16874
  InputRadioButtonModule,
16561
- InputTextModule,
16562
16875
  CoreComponentsTranslationModule,
16563
16876
  InputDatePickerModule,
16564
16877
  InputDateRangePickerModule], exports: [FilterItemComponent] });
@@ -16570,9 +16883,7 @@ class FilterItemModule {
16570
16883
  ScrollingModule,
16571
16884
  AppendPipeModule,
16572
16885
  PrependPipeModule,
16573
- InputTextModule,
16574
16886
  InputRadioButtonModule,
16575
- InputTextModule,
16576
16887
  CoreComponentsTranslationModule,
16577
16888
  InputDatePickerModule,
16578
16889
  InputDateRangePickerModule] });
@@ -16589,9 +16900,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
16589
16900
  ScrollingModule,
16590
16901
  AppendPipeModule,
16591
16902
  PrependPipeModule,
16592
- InputTextModule,
16593
16903
  InputRadioButtonModule,
16594
- InputTextModule,
16595
16904
  CoreComponentsTranslationModule,
16596
16905
  InputDatePickerModule,
16597
16906
  InputDateRangePickerModule