@nexterajs/ui 0.0.1 → 0.1.0

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.
Files changed (63) hide show
  1. package/README.md +279 -34
  2. package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
  3. package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
  4. package/fesm2022/nexterajs-ui-core.mjs +835 -11
  5. package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
  6. package/fesm2022/nexterajs-ui-display.mjs +1142 -0
  7. package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
  8. package/fesm2022/nexterajs-ui-dropdowns.mjs +621 -69
  9. package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
  10. package/fesm2022/nexterajs-ui-file-uploader.mjs +576 -11
  11. package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
  12. package/fesm2022/nexterajs-ui-forms.mjs +2702 -452
  13. package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
  14. package/fesm2022/nexterajs-ui-grid.mjs +2350 -336
  15. package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
  16. package/fesm2022/nexterajs-ui-intl.mjs +117 -15
  17. package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
  18. package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
  19. package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
  20. package/fesm2022/nexterajs-ui-notify.mjs +353 -0
  21. package/fesm2022/nexterajs-ui-notify.mjs.map +1 -0
  22. package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
  23. package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
  24. package/fesm2022/nexterajs-ui-pivot.mjs +191 -136
  25. package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
  26. package/fesm2022/nexterajs-ui-shell.mjs +542 -0
  27. package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
  28. package/fesm2022/nexterajs-ui-styling.mjs +568 -3
  29. package/fesm2022/nexterajs-ui-styling.mjs.map +1 -1
  30. package/fesm2022/nexterajs-ui-tree-view.mjs +150 -18
  31. package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
  32. package/fesm2022/nexterajs-ui-validation.mjs +283 -97
  33. package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
  34. package/fesm2022/nexterajs-ui.mjs +6 -0
  35. package/fesm2022/nexterajs-ui.mjs.map +1 -1
  36. package/package.json +37 -2
  37. package/styles/_dropdown.scss +150 -1
  38. package/styles/_editor.scss +75 -21
  39. package/styles/_icons.scss +92 -0
  40. package/styles/_mixins.scss +104 -1
  41. package/styles/_scrollbar.scss +134 -0
  42. package/styles/_status.scss +21 -0
  43. package/styles/_toast.scss +243 -0
  44. package/styles/_tokens.scss +467 -2
  45. package/styles/_widget.scss +14 -3
  46. package/styles/index.scss +10 -4
  47. package/types/nexterajs-ui-charts.d.ts +446 -0
  48. package/types/nexterajs-ui-core.d.ts +515 -30
  49. package/types/nexterajs-ui-display.d.ts +720 -0
  50. package/types/nexterajs-ui-dropdowns.d.ts +318 -57
  51. package/types/nexterajs-ui-file-uploader.d.ts +240 -8
  52. package/types/nexterajs-ui-forms.d.ts +772 -43
  53. package/types/nexterajs-ui-grid.d.ts +878 -135
  54. package/types/nexterajs-ui-intl.d.ts +39 -4
  55. package/types/nexterajs-ui-navigation.d.ts +531 -0
  56. package/types/nexterajs-ui-notify.d.ts +149 -0
  57. package/types/nexterajs-ui-overlay.d.ts +703 -0
  58. package/types/nexterajs-ui-pivot.d.ts +91 -70
  59. package/types/nexterajs-ui-shell.d.ts +294 -0
  60. package/types/nexterajs-ui-styling.d.ts +163 -3
  61. package/types/nexterajs-ui-tree-view.d.ts +56 -4
  62. package/types/nexterajs-ui-validation.d.ts +110 -22
  63. package/types/nexterajs-ui.d.ts +6 -0
@@ -1,13 +1,18 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, input, booleanAttribute, computed, output, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, forwardRef, ViewContainerRef, viewChild } from '@angular/core';
3
- import { NxeValidationGroupRegistry, NxeFormControlBase, nxeOptionalBoolean, NXE_VALIDATION_TARGET } from '@nexterajs/ui/core';
4
- import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
- import { parseNumberFormat, NXE_NUMBER_SEPARATORS, formatNumber, parseNumber, roundHalfAwayFromZero, parseDateValue, NXE_DEFAULT_DISPLAY_FORMAT, NXE_DEFAULT_DATETIME_DISPLAY_FORMAT, NXE_DEFAULT_TIME_DISPLAY_FORMAT, formatDisplay, buildCalendarWeeks, buildPeriodCells, parseDisplay, serializeDate } from '@nexterajs/ui/intl';
2
+ import { inject, ElementRef, input, booleanAttribute, computed, output, ViewEncapsulation, ChangeDetectionStrategy, Component, linkedSignal, signal, viewChild, forwardRef, numberAttribute, afterRenderEffect, DestroyRef, ViewContainerRef, InjectionToken, Injector, afterNextRender, makeEnvironmentProviders, contentChild, contentChildren, TemplateRef, Directive } from '@angular/core';
3
+ import { NxeValidationGroupRegistry, NXE_VALIDATION_GROUP, NxeFormControlBase, nxeOptionalBoolean, NXE_VALIDATION_TARGET, NxeMessageService, nxeFormatFileSize, nxeResolveExpr, nxeResolveText } from '@nexterajs/ui/core';
4
+ import { NG_VALUE_ACCESSOR, ControlContainer, NgControl, FormSubmittedEvent, FormResetEvent, Validators } from '@angular/forms';
5
+ import { nxeCurrencyCode, parseNumberFormat, nxeCurrencyNumberFormat, NXE_NUMBER_SEPARATORS, formatCurrency, formatNumber, parseNumber, roundHalfAwayFromZero, parseDateValue, NXE_DEFAULT_DISPLAY_FORMAT, NXE_DEFAULT_DATETIME_DISPLAY_FORMAT, NXE_DEFAULT_TIME_DISPLAY_FORMAT, formatDisplay, buildCalendarWeeks, buildPeriodCells, parseDisplay, serializeDate } from '@nexterajs/ui/intl';
6
6
  import { Overlay } from '@angular/cdk/overlay';
7
7
  import { TemplatePortal } from '@angular/cdk/portal';
8
- import { DateTime } from 'luxon';
9
- import { merge, filter } from 'rxjs';
8
+ import { DateTime, Info } from 'luxon';
9
+ import { merge, filter, map } from 'rxjs';
10
10
  import { nxePane, trackPaneFlip } from '@nexterajs/ui/styling';
11
+ import { CdkTrapFocus } from '@angular/cdk/a11y';
12
+ import { BreakpointObserver } from '@angular/cdk/layout';
13
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
14
+ import { FormField } from '@angular/forms/signals';
15
+ import { NgTemplateOutlet } from '@angular/common';
11
16
 
12
17
  /**
13
18
  * dx size coercion: `height="35"` and `[height]="35"` both mean 35px, while `'100%'` or `'3rem'`
@@ -22,26 +27,44 @@ function cssSize(value) {
22
27
  /**
23
28
  * `nxe-button` — drop-in replacement for DevExtreme `dx-button`.
24
29
  *
25
- * The host element IS the button (mirrors DevExtreme's `dx-button` DOM: the host carries the
26
- * styling classes and contains a single `.dx-button-content` child). This keeps every existing
27
- * `.nxe-btn*` / `.dx-button-content` global rule matching verbatim, and — critically — keeps the
28
- * shared `buttonLoading` directive working, which swaps `.dx-button-content` innerHTML for a
29
- * spinner (`getElementsByClassName('dx-button-content')[0]`).
30
+ * The host element IS the button (mirrors DevExtreme's `nxe-button` DOM: the host carries the
31
+ * styling classes and contains a single `.nxe-button-content` child). This keeps every existing
32
+ * `.nxe-btn*` / `.nxe-button-content` global rule matching verbatim, and — critically — keeps the
33
+ * shared `buttonLoading` directive working, which swaps `.nxe-button-content` innerHTML for a
34
+ * spinner (`getElementsByClassName('nxe-button-content')[0]`).
30
35
  *
31
36
  * Content is projected (`<nxe-icon> + <span>`, the dominant pattern). The legacy `text`/`type`
32
- * inputs render a `.dx-button-text` label for the ~25 attribute-styled buttons.
37
+ * inputs render a `.nxe-button-text` label for the ~25 attribute-styled buttons.
38
+ *
39
+ * `stylingMode`, `size`, `loading` and `block` are additive: with none of them set the host renders
40
+ * exactly as before. An icon can be projected into the leading slot as
41
+ * `<svg nxeButtonIcon ...>` (or `[nxeIcon]`), placed before the label.
33
42
  *
34
43
  * `onClick` emits {@link NxeButtonClickEvent}; native `(click)` also fires (27 sites bind it).
35
44
  */
36
45
  class NxeButtonComponent {
37
46
  registry = inject(NxeValidationGroupRegistry);
47
+ groupScope = inject(NXE_VALIDATION_GROUP, { optional: true });
38
48
  elementRef = inject(ElementRef);
39
- /** Legacy `text` label (rendered as `.dx-button-text` when set, no projected content). */
49
+ /** Legacy `text` label (rendered as `.nxe-button-text` when set, no projected content). */
40
50
  text = input(/* @ts-ignore */
41
51
  ...(ngDevMode ? [undefined, { debugName: "text" }] : /* istanbul ignore next */ []));
42
52
  /** Style intent (`default`=blue, `success`=green, `danger`=red, `normal`=white). */
43
53
  type = input('normal', /* @ts-ignore */
44
54
  ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
55
+ /** Fill treatment: `contained` (default), `outlined` (transparent with a coloured border) or `text` (no border). */
56
+ stylingMode = input('contained', /* @ts-ignore */
57
+ ...(ngDevMode ? [{ debugName: "stylingMode" }] : /* istanbul ignore next */ []));
58
+ /** Scale: `sm`, `md` (default, the legacy size) or `lg`. */
59
+ size = input('md', /* @ts-ignore */
60
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
61
+ /**
62
+ * In-flight state: shows a spinner over the (hidden but still laid out, so the width holds)
63
+ * content, sets `aria-busy="true"` and swallows clicks and Enter/Space until it clears.
64
+ */
65
+ loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
66
+ /** Full-width button (`display: flex; width: 100%`). An explicit `width` still wins. */
67
+ block = input(false, { ...(ngDevMode ? { debugName: "block" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
45
68
  /** Disabled state (`[disabled]`); blocks `onClick` and dims the button. */
46
69
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
47
70
  /** Named validation group resolved via {@link NxeValidationGroupRegistry} on click. */
@@ -50,8 +73,8 @@ class NxeButtonComponent {
50
73
  /** DevExtreme parity input; only ever `false` in this codebase (suppresses tab focus). */
51
74
  focusStateEnabled = input(true, { ...(ngDevMode ? { debugName: "focusStateEnabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
52
75
  /**
53
- * dx `icon` — renders `<i class="dx-icon dx-icon-NAME">` as the first child of
54
- * `.dx-button-content`, before any projected content. Projection is the dominant pattern
76
+ * dx `icon` — renders `<i class="nxe-icon nxe-icon-NAME">` as the first child of
77
+ * `.nxe-button-content`, before any projected content. Projection is the dominant pattern
55
78
  * here (a `mat-icon` plus a `span`), so the two must coexist rather than compete.
56
79
  */
57
80
  icon = input(undefined, /* @ts-ignore */
@@ -74,12 +97,15 @@ class NxeButtonComponent {
74
97
  this.handleClick(event);
75
98
  }
76
99
  handleClick(event) {
77
- if (this.disabled()) {
100
+ if (this.disabled() || this.loading()) {
78
101
  event.preventDefault();
79
102
  event.stopImmediatePropagation();
80
103
  return;
81
104
  }
82
- const group = this.validationGroup() ?? '';
105
+ // A named group wins; otherwise a surrounding nxe-validation-group; otherwise dx's
106
+ // implicit global group. Same precedence the validator applies, so a button and the
107
+ // fields beside it always resolve to the same group.
108
+ const group = this.validationGroup() || this.groupScope?.groupKey || '';
83
109
  this.onClick.emit({
84
110
  event,
85
111
  validationGroup: { validate: () => this.registry.validate(group) },
@@ -88,49 +114,63 @@ class NxeButtonComponent {
88
114
  });
89
115
  }
90
116
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
91
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeButtonComponent, isStandalone: true, selector: "nxe-button", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, validationGroup: { classPropertyName: "validationGroup", publicName: "validationGroup", isSignal: true, isRequired: false, transformFunction: null }, focusStateEnabled: { classPropertyName: "focusStateEnabled", publicName: "focusStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, host: { attributes: { "role": "button" }, listeners: { "click": "handleClick($event)", "keydown.enter": "handleKeydown($event)", "keydown.space": "handleKeydown($event)" }, properties: { "attr.tabindex": "disabled() || !focusStateEnabled() ? null : 0", "attr.aria-disabled": "disabled() ? true : null", "class.nxe-button-default": "type() === 'default'", "class.nxe-button-success": "type() === 'success'", "class.nxe-button-danger": "type() === 'danger'", "class.nxe-button-has-text": "!!text()", "class.dx-button-has-text": "!!text()", "class.dx-button-has-icon": "!!icon()", "style.height": "heightStyle()", "style.width": "widthStyle()", "class.nxe-disabled": "disabled()" }, classAttribute: "nxe-widget" }, ngImport: i0, template: `
92
- <span class="dx-button-content">
117
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeButtonComponent, isStandalone: true, selector: "nxe-button", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, stylingMode: { classPropertyName: "stylingMode", publicName: "stylingMode", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, validationGroup: { classPropertyName: "validationGroup", publicName: "validationGroup", isSignal: true, isRequired: false, transformFunction: null }, focusStateEnabled: { classPropertyName: "focusStateEnabled", publicName: "focusStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, host: { attributes: { "role": "button" }, listeners: { "click": "handleClick($event)", "keydown.enter": "handleKeydown($event)", "keydown.space": "handleKeydown($event)" }, properties: { "attr.tabindex": "disabled() || !focusStateEnabled() ? null : 0", "attr.aria-disabled": "disabled() || loading() ? true : null", "attr.aria-busy": "loading() ? true : null", "class.nxe-button-mode-outlined": "stylingMode() === 'outlined'", "class.nxe-button-mode-text": "stylingMode() === 'text'", "class.nxe-button-sm": "size() === 'sm'", "class.nxe-button-lg": "size() === 'lg'", "class.nxe-button-block": "block()", "class.nxe-button-loading": "loading()", "class.nxe-button-default": "type() === 'default'", "class.nxe-button-success": "type() === 'success'", "class.nxe-button-danger": "type() === 'danger'", "class.nxe-button-has-text": "!!text()", "class.nxe-button-has-icon": "!!icon()", "style.height": "heightStyle()", "style.width": "widthStyle()", "class.nxe-disabled": "disabled()" }, classAttribute: "nxe-widget" }, ngImport: i0, template: `
118
+ <span class="nxe-button-content">
93
119
  @if (icon()) {
94
- <i class="dx-icon dx-icon-{{ icon() }}"></i>
120
+ <i class="nxe-icon nxe-icon-{{ icon() }}"></i>
95
121
  }
122
+ <ng-content select="[nxeButtonIcon], [nxeIcon]" />
96
123
  <ng-content />
97
124
  @if (text()) {
98
- <span class="dx-button-text">{{ text() }}</span>
125
+ <span class="nxe-button-text">{{ text() }}</span>
99
126
  }
100
127
  </span>
101
- `, isInline: true, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid #ddd;border-radius:4px;background-color:#fff;color:#333;font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button .dx-button-content{flex:1 1 auto;display:flex;align-items:center;justify-content:center;gap:4px;line-height:1.35715;padding:8px}nxe-button.nxe-button-has-text .dx-button-content{padding-inline:12px}nxe-button .dx-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:#f4f4f4}nxe-button:active{background-color:#c2c2c2}nxe-button.nxe-button-default{background-color:#337ab7;border-color:transparent;color:#fff}nxe-button.nxe-button-default:hover{background-color:#2d6ca3}nxe-button.nxe-button-default:active{background-color:#19405b}nxe-button.nxe-button-success{background-color:#5cb85c;border-color:transparent;color:#fff}nxe-button.nxe-button-success:hover{background-color:#46a046}nxe-button.nxe-button-success:active{background-color:#306e30}nxe-button.nxe-button-danger{background-color:#d9534f;border-color:transparent;color:#fff}nxe-button.nxe-button-danger:hover{background-color:#cd312c}nxe-button.nxe-button-danger:active{background-color:#932320}nxe-button[disabled]{opacity:.5;cursor:default}nxe-button.nxe-disabled{opacity:.5;cursor:default;pointer-events:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
128
+ @if (loading()) {
129
+ <span class="nxe-button-spinner" aria-hidden="true"></span>
130
+ }
131
+ `, isInline: true, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd));border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-button-bg-secondary, var(--nxe-ui-surface, #fff));color:var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button:where(:focus-visible){box-shadow:var(--nxe-ui-button-focus-ring, none)}nxe-button .nxe-button-content{box-sizing:border-box;flex:1 1 auto;height:100%;max-height:100%;display:flex;align-items:center;justify-content:center;gap:var(--nxe-ui-button-gap, 0px);line-height:1.35715;padding:var(--nxe-ui-button-padding, 8px)}nxe-button.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-button-padding-inline, 12px)}nxe-button.nxe-button-has-text .nxe-button-content>i.nxe-icon{margin-inline-start:0;margin-inline-end:calc(9px - var(--nxe-ui-button-gap, 0px))}nxe-button .nxe-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:var(--nxe-ui-button-hover-bg-secondary, #f4f4f4)}nxe-button:active{background-color:var(--nxe-ui-button-active-bg-secondary, #c2c2c2)}nxe-button.nxe-button-default{background-color:var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7));border-color:var(--nxe-ui-button-border-primary, transparent);color:var(--nxe-ui-button-fg-primary, var(--nxe-ui-accent-contrast, #fff))}nxe-button.nxe-button-default:hover{background-color:var(--nxe-ui-button-hover-bg-primary, #2d6ca3)}nxe-button.nxe-button-default:active{background-color:var(--nxe-ui-button-active-bg-primary, #19405b)}nxe-button.nxe-button-success{background-color:var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c));border-color:var(--nxe-ui-button-border-success, transparent);color:var(--nxe-ui-button-fg-success, #fff)}nxe-button.nxe-button-success:hover{background-color:var(--nxe-ui-button-hover-bg-success, #46a046)}nxe-button.nxe-button-success:active{background-color:var(--nxe-ui-button-active-bg-success, #306e30)}nxe-button.nxe-button-danger{background-color:var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f));border-color:var(--nxe-ui-button-border-danger, transparent);color:var(--nxe-ui-button-fg-danger, #fff)}nxe-button.nxe-button-danger:hover{background-color:var(--nxe-ui-button-hover-bg-danger, #cd312c)}nxe-button.nxe-button-danger:active{background-color:var(--nxe-ui-button-active-bg-danger, #932320)}nxe-button[disabled]{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default}nxe-button.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default;pointer-events:none}nxe-button .nxe-button-content>[nxeButtonIcon],nxe-button .nxe-button-content>[nxeIcon]{flex:none;width:1.25em;height:1.25em}nxe-button .nxe-button-content>[nxeButtonIcon]:not(:last-child),nxe-button .nxe-button-content>[nxeIcon]:not(:last-child){margin-inline-end:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-sm{font-size:var(--nxe-ui-font-size-sm, 13px)}nxe-button.nxe-button-sm .nxe-button-content{padding:var(--nxe-ui-space-1, 4px)}nxe-button.nxe-button-sm.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-lg{font-size:var(--nxe-ui-font-size-lg, 16px)}nxe-button.nxe-button-lg .nxe-button-content{padding:var(--nxe-ui-space-3, 12px)}nxe-button.nxe-button-lg.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-5, 20px)}nxe-button.nxe-button-block{display:flex;width:100%}nxe-button.nxe-button-mode-outlined,nxe-button.nxe-button-mode-text{--nxe-button-tone: var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined:hover,nxe-button.nxe-button-mode-text:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined:active,nxe-button.nxe-button-mode-text:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-default{--nxe-button-tone: var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7))}nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-success{--nxe-button-tone: var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c))}nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-danger{--nxe-button-tone: var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined.nxe-button-default:hover,nxe-button.nxe-button-mode-outlined.nxe-button-success:hover,nxe-button.nxe-button-mode-outlined.nxe-button-danger:hover,nxe-button.nxe-button-mode-text.nxe-button-default:hover,nxe-button.nxe-button-mode-text.nxe-button-success:hover,nxe-button.nxe-button-mode-text.nxe-button-danger:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default:active,nxe-button.nxe-button-mode-outlined.nxe-button-success:active,nxe-button.nxe-button-mode-outlined.nxe-button-danger:active,nxe-button.nxe-button-mode-text.nxe-button-default:active,nxe-button.nxe-button-mode-text.nxe-button-success:active,nxe-button.nxe-button-mode-text.nxe-button-danger:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined{border-color:var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger{border-color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-text,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{border-color:transparent}nxe-button.nxe-button-loading{position:relative;cursor:progress}nxe-button.nxe-button-loading .nxe-button-content{visibility:hidden}nxe-button .nxe-button-spinner{position:absolute;inset:0;margin:auto;box-sizing:border-box;width:1.25em;height:1.25em;border:2px solid currentColor;border-inline-end-color:transparent;border-radius:50%;animation:nxe-button-spin .7s linear infinite}@keyframes nxe-button-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){nxe-button .nxe-button-spinner{animation-duration:2s}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
102
132
  }
103
133
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeButtonComponent, decorators: [{
104
134
  type: Component,
105
135
  args: [{ selector: 'nxe-button', standalone: true, template: `
106
- <span class="dx-button-content">
136
+ <span class="nxe-button-content">
107
137
  @if (icon()) {
108
- <i class="dx-icon dx-icon-{{ icon() }}"></i>
138
+ <i class="nxe-icon nxe-icon-{{ icon() }}"></i>
109
139
  }
140
+ <ng-content select="[nxeButtonIcon], [nxeIcon]" />
110
141
  <ng-content />
111
142
  @if (text()) {
112
- <span class="dx-button-text">{{ text() }}</span>
143
+ <span class="nxe-button-text">{{ text() }}</span>
113
144
  }
114
145
  </span>
146
+ @if (loading()) {
147
+ <span class="nxe-button-spinner" aria-hidden="true"></span>
148
+ }
115
149
  `, host: {
116
150
  class: 'nxe-widget',
117
151
  role: 'button',
118
152
  '[attr.tabindex]': 'disabled() || !focusStateEnabled() ? null : 0',
119
- '[attr.aria-disabled]': 'disabled() ? true : null',
153
+ '[attr.aria-disabled]': 'disabled() || loading() ? true : null',
154
+ '[attr.aria-busy]': 'loading() ? true : null',
155
+ '[class.nxe-button-mode-outlined]': "stylingMode() === 'outlined'",
156
+ '[class.nxe-button-mode-text]': "stylingMode() === 'text'",
157
+ '[class.nxe-button-sm]': "size() === 'sm'",
158
+ '[class.nxe-button-lg]': "size() === 'lg'",
159
+ '[class.nxe-button-block]': 'block()',
160
+ '[class.nxe-button-loading]': 'loading()',
120
161
  '[class.nxe-button-default]': "type() === 'default'",
121
162
  '[class.nxe-button-success]': "type() === 'success'",
122
163
  '[class.nxe-button-danger]': "type() === 'danger'",
123
164
  '[class.nxe-button-has-text]': '!!text()',
124
- '[class.dx-button-has-text]': '!!text()',
125
- '[class.dx-button-has-icon]': '!!icon()',
165
+ '[class.nxe-button-has-icon]': '!!icon()',
126
166
  '[style.height]': 'heightStyle()',
127
167
  '[style.width]': 'widthStyle()',
128
168
  '[class.nxe-disabled]': 'disabled()',
129
169
  '(click)': 'handleClick($event)',
130
170
  '(keydown.enter)': 'handleKeydown($event)',
131
171
  '(keydown.space)': 'handleKeydown($event)',
132
- }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid #ddd;border-radius:4px;background-color:#fff;color:#333;font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button .dx-button-content{flex:1 1 auto;display:flex;align-items:center;justify-content:center;gap:4px;line-height:1.35715;padding:8px}nxe-button.nxe-button-has-text .dx-button-content{padding-inline:12px}nxe-button .dx-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:#f4f4f4}nxe-button:active{background-color:#c2c2c2}nxe-button.nxe-button-default{background-color:#337ab7;border-color:transparent;color:#fff}nxe-button.nxe-button-default:hover{background-color:#2d6ca3}nxe-button.nxe-button-default:active{background-color:#19405b}nxe-button.nxe-button-success{background-color:#5cb85c;border-color:transparent;color:#fff}nxe-button.nxe-button-success:hover{background-color:#46a046}nxe-button.nxe-button-success:active{background-color:#306e30}nxe-button.nxe-button-danger{background-color:#d9534f;border-color:transparent;color:#fff}nxe-button.nxe-button-danger:hover{background-color:#cd312c}nxe-button.nxe-button-danger:active{background-color:#932320}nxe-button[disabled]{opacity:.5;cursor:default}nxe-button.nxe-disabled{opacity:.5;cursor:default;pointer-events:none}\n"] }]
133
- }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], validationGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationGroup", required: false }] }], focusStateEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusStateEnabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }] } });
172
+ }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd));border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-button-bg-secondary, var(--nxe-ui-surface, #fff));color:var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button:where(:focus-visible){box-shadow:var(--nxe-ui-button-focus-ring, none)}nxe-button .nxe-button-content{box-sizing:border-box;flex:1 1 auto;height:100%;max-height:100%;display:flex;align-items:center;justify-content:center;gap:var(--nxe-ui-button-gap, 0px);line-height:1.35715;padding:var(--nxe-ui-button-padding, 8px)}nxe-button.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-button-padding-inline, 12px)}nxe-button.nxe-button-has-text .nxe-button-content>i.nxe-icon{margin-inline-start:0;margin-inline-end:calc(9px - var(--nxe-ui-button-gap, 0px))}nxe-button .nxe-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:var(--nxe-ui-button-hover-bg-secondary, #f4f4f4)}nxe-button:active{background-color:var(--nxe-ui-button-active-bg-secondary, #c2c2c2)}nxe-button.nxe-button-default{background-color:var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7));border-color:var(--nxe-ui-button-border-primary, transparent);color:var(--nxe-ui-button-fg-primary, var(--nxe-ui-accent-contrast, #fff))}nxe-button.nxe-button-default:hover{background-color:var(--nxe-ui-button-hover-bg-primary, #2d6ca3)}nxe-button.nxe-button-default:active{background-color:var(--nxe-ui-button-active-bg-primary, #19405b)}nxe-button.nxe-button-success{background-color:var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c));border-color:var(--nxe-ui-button-border-success, transparent);color:var(--nxe-ui-button-fg-success, #fff)}nxe-button.nxe-button-success:hover{background-color:var(--nxe-ui-button-hover-bg-success, #46a046)}nxe-button.nxe-button-success:active{background-color:var(--nxe-ui-button-active-bg-success, #306e30)}nxe-button.nxe-button-danger{background-color:var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f));border-color:var(--nxe-ui-button-border-danger, transparent);color:var(--nxe-ui-button-fg-danger, #fff)}nxe-button.nxe-button-danger:hover{background-color:var(--nxe-ui-button-hover-bg-danger, #cd312c)}nxe-button.nxe-button-danger:active{background-color:var(--nxe-ui-button-active-bg-danger, #932320)}nxe-button[disabled]{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default}nxe-button.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default;pointer-events:none}nxe-button .nxe-button-content>[nxeButtonIcon],nxe-button .nxe-button-content>[nxeIcon]{flex:none;width:1.25em;height:1.25em}nxe-button .nxe-button-content>[nxeButtonIcon]:not(:last-child),nxe-button .nxe-button-content>[nxeIcon]:not(:last-child){margin-inline-end:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-sm{font-size:var(--nxe-ui-font-size-sm, 13px)}nxe-button.nxe-button-sm .nxe-button-content{padding:var(--nxe-ui-space-1, 4px)}nxe-button.nxe-button-sm.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-lg{font-size:var(--nxe-ui-font-size-lg, 16px)}nxe-button.nxe-button-lg .nxe-button-content{padding:var(--nxe-ui-space-3, 12px)}nxe-button.nxe-button-lg.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-5, 20px)}nxe-button.nxe-button-block{display:flex;width:100%}nxe-button.nxe-button-mode-outlined,nxe-button.nxe-button-mode-text{--nxe-button-tone: var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined:hover,nxe-button.nxe-button-mode-text:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined:active,nxe-button.nxe-button-mode-text:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-default{--nxe-button-tone: var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7))}nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-success{--nxe-button-tone: var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c))}nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-danger{--nxe-button-tone: var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined.nxe-button-default:hover,nxe-button.nxe-button-mode-outlined.nxe-button-success:hover,nxe-button.nxe-button-mode-outlined.nxe-button-danger:hover,nxe-button.nxe-button-mode-text.nxe-button-default:hover,nxe-button.nxe-button-mode-text.nxe-button-success:hover,nxe-button.nxe-button-mode-text.nxe-button-danger:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default:active,nxe-button.nxe-button-mode-outlined.nxe-button-success:active,nxe-button.nxe-button-mode-outlined.nxe-button-danger:active,nxe-button.nxe-button-mode-text.nxe-button-default:active,nxe-button.nxe-button-mode-text.nxe-button-success:active,nxe-button.nxe-button-mode-text.nxe-button-danger:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined{border-color:var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger{border-color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-text,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{border-color:transparent}nxe-button.nxe-button-loading{position:relative;cursor:progress}nxe-button.nxe-button-loading .nxe-button-content{visibility:hidden}nxe-button .nxe-button-spinner{position:absolute;inset:0;margin:auto;box-sizing:border-box;width:1.25em;height:1.25em;border:2px solid currentColor;border-inline-end-color:transparent;border-radius:50%;animation:nxe-button-spin .7s linear infinite}@keyframes nxe-button-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){nxe-button .nxe-button-spinner{animation-duration:2s}}\n"] }]
173
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], stylingMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "stylingMode", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], validationGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationGroup", required: false }] }], focusStateEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusStateEnabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }] } });
134
174
 
135
175
  /**
136
176
  * `nxe-text-box` — drop-in replacement for DevExtreme `dx-text-box`.
@@ -139,6 +179,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
139
179
  * `[(ngModel)]`, one-way `[value]`, and `[readOnly]`. Like DevExtreme's default
140
180
  * (`valueChangeEvent='change'`), the value commits on **blur** — not on every keystroke.
141
181
  *
182
+ * `showClearButton` (dx default `false`) adds dx's clear area after the input while the field
183
+ * holds text and is neither read-only nor disabled; it sets the value to `''`, dx's own empty
184
+ * value for a text box, and focuses the input.
185
+ *
142
186
  * It provides {@link NXE_VALIDATION_TARGET} so a projected `nxe-validator` can read its value
143
187
  * and push back the invalid message, which renders as the red `.nxe-invalid-message` bubble.
144
188
  */
@@ -152,38 +196,132 @@ class NxeTextBoxComponent extends NxeFormControlBase {
152
196
  hoverStateEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "hoverStateEnabledInput" } : /* istanbul ignore next */ {}), alias: 'hoverStateEnabled', transform: nxeOptionalBoolean });
153
197
  hoverStateEnabled = computed(() => this.hoverStateEnabledInput() ?? this.nxeDefaults.textBox?.hoverStateEnabled ?? false, /* @ts-ignore */
154
198
  ...(ngDevMode ? [{ debugName: "hoverStateEnabled" }] : /* istanbul ignore next */ []));
199
+ /** dx `showClearButton` — off by default, as in dx; `NXE_DEFAULT_OPTIONS.textBox` can turn it on app-wide. */
200
+ showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
201
+ showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.textBox?.showClearButton ?? false, /* @ts-ignore */
202
+ ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
203
+ /**
204
+ * With `mode="password"`, an icon button after the field that shows the password as text and
205
+ * hides it again. Off by default; it has no effect in any other mode.
206
+ */
207
+ showPasswordToggle = input(false, { ...(ngDevMode ? { debugName: "showPasswordToggle" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
208
+ passwordToggleVisible = computed(() => this.showPasswordToggle() && this.mode() === 'password' && !this.disabled(), /* @ts-ignore */
209
+ ...(ngDevMode ? [{ debugName: "passwordToggleVisible" }] : /* istanbul ignore next */ []));
210
+ /** Whether the password is showing as text right now; hidden again whenever the toggle goes away. */
211
+ passwordRevealed = linkedSignal({ ...(ngDevMode ? { debugName: "passwordRevealed" } : /* istanbul ignore next */ {}), source: this.passwordToggleVisible,
212
+ computation: () => false });
213
+ passwordToggleLabel = computed(() => this.nxeMessages.t(this.passwordRevealed() ? 'forms.password.hide' : 'forms.password.show'), /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "passwordToggleLabel" }] : /* istanbul ignore next */ []));
155
215
  focused = signal(false, /* @ts-ignore */
156
216
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
157
217
  displayValue = computed(() => this.value() ?? '', /* @ts-ignore */
158
218
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
219
+ fieldRef = viewChild.required('field', /* @ts-ignore */
220
+ ...(ngDevMode ? [{ debugName: "fieldRef" }] : /* istanbul ignore next */ []));
221
+ /**
222
+ * The text in the field right now, typed or not. dx shows the clear button by the field's
223
+ * emptiness (`.dx-texteditor-empty`), which follows every keystroke, not the committed value.
224
+ */
225
+ fieldText = linkedSignal(() => this.displayValue(), /* @ts-ignore */
226
+ ...(ngDevMode ? [{ debugName: "fieldText" }] : /* istanbul ignore next */ []));
227
+ clearButtonVisible = computed(() => this.showClearButton() && !this.readOnly() && !this.disabled() && this.fieldText() !== '', /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "clearButtonVisible" }] : /* istanbul ignore next */ []));
229
+ /** dx `valueChangeEvent: 'input'` — the search-as-you-type case. */
230
+ onTypedInput(event) {
231
+ const text = event.target.value;
232
+ this.fieldText.set(text);
233
+ this.commitOn('input', text, event);
234
+ }
235
+ /**
236
+ * dx `_clearValueHandler`: value back to `''`, then focus on the input. The field is emptied by
237
+ * hand as well — typed-but-uncommitted text over an already-empty value would otherwise survive,
238
+ * because the `[value]` binding has not changed.
239
+ */
240
+ onClear(event) {
241
+ const field = this.fieldRef().nativeElement;
242
+ this.commitValue('', event);
243
+ field.value = '';
244
+ this.fieldText.set('');
245
+ if (!this.focused()) {
246
+ field.focus();
247
+ }
248
+ }
249
+ onTypedKeyup(event) {
250
+ this.commitOn('keyup', event.target.value, event);
251
+ }
252
+ commitPending(event) {
253
+ this.commitValue(this.fieldRef().nativeElement.value, event);
254
+ }
159
255
  onBlur(event) {
256
+ // Blur commits whatever the mode: a value typed and left must not be discarded because
257
+ // the configured event never fired.
160
258
  this.commitValue(event.target.value, event);
161
259
  this.focused.set(false);
162
260
  this.markAsTouched();
163
261
  }
164
262
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
165
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextBoxComponent, isStandalone: true, selector: "nxe-text-box", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabledInput: { classPropertyName: "hoverStateEnabledInput", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-textbox" }, providers: [
263
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextBoxComponent, isStandalone: true, selector: "nxe-text-box", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabledInput: { classPropertyName: "hoverStateEnabledInput", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showPasswordToggle: { classPropertyName: "showPasswordToggle", publicName: "showPasswordToggle", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-textbox" }, providers: [
166
264
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextBoxComponent), multi: true },
167
265
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextBoxComponent) },
168
- ], usesInheritance: true, ngImport: i0, template: `
169
- <div class="nxe-editor-container">
170
- <input
171
- class="nxe-editor-input nxe-textbox-input"
172
- [type]="mode()"
173
- [value]="displayValue()"
174
- [readOnly]="readOnly()"
175
- [disabled]="disabled()"
176
- [attr.placeholder]="placeholder() || null"
177
- autocomplete="off"
178
- (blur)="onBlur($event)"
179
- (focus)="focused.set(true)" />
180
- </div>
181
- <ng-content />
182
- @if (validationError(); as error) {
183
- @if (focused()) {
184
- <div class="nxe-invalid-message">{{ error }}</div>
185
- }
186
- }
266
+ ], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
267
+ <div class="nxe-editor-container">
268
+ <input
269
+ #field
270
+ class="nxe-editor-input nxe-textbox-input"
271
+ [attr.tabindex]="tabIndex()"
272
+ [id]="controlId()"
273
+ [attr.aria-label]="ariaLabel() || null"
274
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
275
+ [attr.aria-invalid]="invalidState() ? true : null"
276
+ [attr.aria-required]="required() ? true : null"
277
+ [type]="passwordRevealed() ? 'text' : mode()"
278
+ [value]="displayValue()"
279
+ [readOnly]="readOnly()"
280
+ [disabled]="disabled()"
281
+ [attr.placeholder]="placeholder() || null"
282
+ autocomplete="off"
283
+ (input)="onTypedInput($event)"
284
+ (keyup)="onTypedKeyup($event)"
285
+ (blur)="onBlur($event)"
286
+ (focus)="focused.set(true)" />
287
+ @if (clearButtonVisible()) {
288
+ <span
289
+ class="nxe-clear-button-area"
290
+ (mousedown)="$event.preventDefault()"
291
+ (click)="onClear($event)"
292
+ aria-hidden="true">
293
+ <!-- A Ø16 disc with a 2px × cut out (the dropdown editors' clear glyph). -->
294
+ <svg viewBox="0 0 18 18" focusable="false">
295
+ <path fill-rule="evenodd" transform="rotate(45 9 9)"
296
+ d="M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z" />
297
+ </svg>
298
+ </span>
299
+ }
300
+ @if (passwordToggleVisible()) {
301
+ <!-- mousedown is held back so a click does not pull focus out of the field. -->
302
+ <button
303
+ type="button"
304
+ class="nxe-password-toggle"
305
+ [attr.aria-label]="passwordToggleLabel()"
306
+ [attr.aria-pressed]="passwordRevealed()"
307
+ [attr.aria-controls]="controlId()"
308
+ (mousedown)="$event.preventDefault()"
309
+ (click)="passwordRevealed.set(!passwordRevealed())">
310
+ <svg viewBox="0 0 18 18" focusable="false" aria-hidden="true">
311
+ <path d="M9 3.5C5.2 3.5 2.2 6 1 9c1.2 3 4.2 5.5 8 5.5s6.8-2.5 8-5.5c-1.2-3-4.2-5.5-8-5.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0-5.2a1.7 1.7 0 1 0 0 3.4 1.7 1.7 0 0 0 0-3.4Z" />
312
+ @if (passwordRevealed()) {
313
+ <path class="nxe-password-toggle-slash" d="M3 2.4 15.6 15l-1 1L2 3.4l1-1Z" />
314
+ }
315
+ </svg>
316
+ </button>
317
+ }
318
+ </div>
319
+ <ng-content />
320
+ @if (validationError(); as error) {
321
+ @if (focused()) {
322
+ <div class="nxe-invalid-message">{{ error }}</div>
323
+ }
324
+ }
187
325
  `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
188
326
  }
189
327
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextBoxComponent, decorators: [{
@@ -191,30 +329,70 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
191
329
  args: [{
192
330
  selector: 'nxe-text-box',
193
331
  standalone: true,
194
- template: `
195
- <div class="nxe-editor-container">
196
- <input
197
- class="nxe-editor-input nxe-textbox-input"
198
- [type]="mode()"
199
- [value]="displayValue()"
200
- [readOnly]="readOnly()"
201
- [disabled]="disabled()"
202
- [attr.placeholder]="placeholder() || null"
203
- autocomplete="off"
204
- (blur)="onBlur($event)"
205
- (focus)="focused.set(true)" />
206
- </div>
207
- <ng-content />
208
- @if (validationError(); as error) {
209
- @if (focused()) {
210
- <div class="nxe-invalid-message">{{ error }}</div>
211
- }
212
- }
332
+ template: `
333
+ <div class="nxe-editor-container">
334
+ <input
335
+ #field
336
+ class="nxe-editor-input nxe-textbox-input"
337
+ [attr.tabindex]="tabIndex()"
338
+ [id]="controlId()"
339
+ [attr.aria-label]="ariaLabel() || null"
340
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
341
+ [attr.aria-invalid]="invalidState() ? true : null"
342
+ [attr.aria-required]="required() ? true : null"
343
+ [type]="passwordRevealed() ? 'text' : mode()"
344
+ [value]="displayValue()"
345
+ [readOnly]="readOnly()"
346
+ [disabled]="disabled()"
347
+ [attr.placeholder]="placeholder() || null"
348
+ autocomplete="off"
349
+ (input)="onTypedInput($event)"
350
+ (keyup)="onTypedKeyup($event)"
351
+ (blur)="onBlur($event)"
352
+ (focus)="focused.set(true)" />
353
+ @if (clearButtonVisible()) {
354
+ <span
355
+ class="nxe-clear-button-area"
356
+ (mousedown)="$event.preventDefault()"
357
+ (click)="onClear($event)"
358
+ aria-hidden="true">
359
+ <!-- A Ø16 disc with a 2px × cut out (the dropdown editors' clear glyph). -->
360
+ <svg viewBox="0 0 18 18" focusable="false">
361
+ <path fill-rule="evenodd" transform="rotate(45 9 9)"
362
+ d="M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z" />
363
+ </svg>
364
+ </span>
365
+ }
366
+ @if (passwordToggleVisible()) {
367
+ <!-- mousedown is held back so a click does not pull focus out of the field. -->
368
+ <button
369
+ type="button"
370
+ class="nxe-password-toggle"
371
+ [attr.aria-label]="passwordToggleLabel()"
372
+ [attr.aria-pressed]="passwordRevealed()"
373
+ [attr.aria-controls]="controlId()"
374
+ (mousedown)="$event.preventDefault()"
375
+ (click)="passwordRevealed.set(!passwordRevealed())">
376
+ <svg viewBox="0 0 18 18" focusable="false" aria-hidden="true">
377
+ <path d="M9 3.5C5.2 3.5 2.2 6 1 9c1.2 3 4.2 5.5 8 5.5s6.8-2.5 8-5.5c-1.2-3-4.2-5.5-8-5.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0-5.2a1.7 1.7 0 1 0 0 3.4 1.7 1.7 0 0 0 0-3.4Z" />
378
+ @if (passwordRevealed()) {
379
+ <path class="nxe-password-toggle-slash" d="M3 2.4 15.6 15l-1 1L2 3.4l1-1Z" />
380
+ }
381
+ </svg>
382
+ </button>
383
+ }
384
+ </div>
385
+ <ng-content />
386
+ @if (validationError(); as error) {
387
+ @if (focused()) {
388
+ <div class="nxe-invalid-message">{{ error }}</div>
389
+ }
390
+ }
213
391
  `,
214
392
  host: {
215
393
  class: 'nxe-widget nxe-editor nxe-textbox',
216
394
  '[class.nxe-focused]': 'focused()',
217
- '[class.nxe-invalid]': '!!validationError()',
395
+ '[class.nxe-invalid]': 'invalidState()',
218
396
  },
219
397
  providers: [
220
398
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextBoxComponent), multi: true },
@@ -223,7 +401,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
223
401
  changeDetection: ChangeDetectionStrategy.OnPush,
224
402
  encapsulation: ViewEncapsulation.None,
225
403
  }]
226
- }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hoverStateEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }] } });
404
+ }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hoverStateEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showPasswordToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPasswordToggle", required: false }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
227
405
 
228
406
  /**
229
407
  * `nxe-text-area` — drop-in replacement for DevExtreme `dx-text-area`.
@@ -240,82 +418,237 @@ class NxeTextAreaComponent extends NxeFormControlBase {
240
418
  heightInput = input(undefined, { ...(ngDevMode ? { debugName: "heightInput" } : /* istanbul ignore next */ {}), alias: 'height' });
241
419
  height = computed(() => this.heightInput() ?? this.nxeDefaults.textArea?.height ?? 80, /* @ts-ignore */
242
420
  ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
421
+ /**
422
+ * dx `maxLength`: the most characters the field takes. Sets the native `maxlength`, so typing
423
+ * and pasting stop at the limit, and shows a live "12/200" count under the text. Unset (the
424
+ * default), there is no limit and no counter.
425
+ */
426
+ maxLength = input(undefined, { ...(ngDevMode ? { debugName: "maxLength" } : /* istanbul ignore next */ {}), transform: (v) => (v === null || v === undefined || v === '' ? undefined : numberAttribute(v, undefined)) });
427
+ /**
428
+ * Grow with the text between {@link minRows} and {@link maxRows}, scrolling past the maximum.
429
+ * The fixed `height` is ignored while this is on. Off by default.
430
+ */
431
+ autoGrow = input(false, { ...(ngDevMode ? { debugName: "autoGrow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
432
+ /** Rows the field never shrinks below while `autoGrow` is on. */
433
+ minRows = input(3, { ...(ngDevMode ? { debugName: "minRows" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(1, numberAttribute(v, 3)) });
434
+ /** Rows after which an auto-growing field scrolls instead of growing; `0` means never. */
435
+ maxRows = input(10, { ...(ngDevMode ? { debugName: "maxRows" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(0, numberAttribute(v, 10)) });
243
436
  focused = signal(false, /* @ts-ignore */
244
437
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
245
438
  displayValue = computed(() => this.value() ?? '', /* @ts-ignore */
246
439
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
440
+ /** The text in the field right now, typed or not: the counter and the height follow every keystroke. */
441
+ fieldText = linkedSignal(() => this.displayValue(), /* @ts-ignore */
442
+ ...(ngDevMode ? [{ debugName: "fieldText" }] : /* istanbul ignore next */ []));
443
+ remaining = computed(() => (this.maxLength() ?? 0) - this.fieldText().length, /* @ts-ignore */
444
+ ...(ngDevMode ? [{ debugName: "remaining" }] : /* istanbul ignore next */ []));
445
+ /** Close enough to the limit to say so: the last 10 characters, or 10% of a long limit. */
446
+ nearLimit = computed(() => {
447
+ const max = this.maxLength();
448
+ return max !== undefined && this.remaining() <= Math.max(10, Math.ceil(max * 0.1));
449
+ }, /* @ts-ignore */
450
+ ...(ngDevMode ? [{ debugName: "nearLimit" }] : /* istanbul ignore next */ []));
451
+ /** Spoken by the polite status; empty (so silent) until {@link nearLimit}. */
452
+ limitStatus = computed(() => {
453
+ if (!this.nearLimit()) {
454
+ return '';
455
+ }
456
+ const left = this.remaining();
457
+ return left >= 0
458
+ ? this.nxeMessages.t('forms.textArea.charactersLeft', left)
459
+ : this.nxeMessages.t('forms.textArea.charactersOver', -left);
460
+ }, /* @ts-ignore */
461
+ ...(ngDevMode ? [{ debugName: "limitStatus" }] : /* istanbul ignore next */ []));
462
+ growState = computed(() => this.autoGrow()
463
+ ? { text: this.fieldText(), minRows: this.minRows(), maxRows: this.maxRows() }
464
+ : null, /* @ts-ignore */
465
+ ...(ngDevMode ? [{ debugName: "growState" }] : /* istanbul ignore next */ []));
466
+ constructor() {
467
+ super();
468
+ // Reads exactly `growState()`; the measuring below reads no signal.
469
+ afterRenderEffect(() => this.resize(this.growState()));
470
+ }
471
+ /** dx `valueChangeEvent: 'input'` — the search-as-you-type case. */
472
+ onTypedInput(event) {
473
+ const text = event.target.value;
474
+ this.fieldText.set(text);
475
+ this.commitOn('input', text, event);
476
+ }
477
+ /** Size the textarea to its text: collapse it to measure, then clamp to the row limits. */
478
+ resize(state) {
479
+ const el = this.elementRef.nativeElement.querySelector('textarea');
480
+ if (!el || !state) {
481
+ return;
482
+ }
483
+ const style = getComputedStyle(el);
484
+ const px = (v) => parseFloat(v) || 0;
485
+ const lineHeight = px(style.lineHeight) || px(style.fontSize) * 1.35715;
486
+ const padding = px(style.paddingTop) + px(style.paddingBottom);
487
+ const border = px(style.borderTopWidth) + px(style.borderBottomWidth);
488
+ const chrome = style.boxSizing === 'border-box' ? padding + border : 0;
489
+ el.style.height = '0px';
490
+ const content = el.scrollHeight - padding;
491
+ const min = state.minRows * lineHeight;
492
+ const max = state.maxRows > 0 ? state.maxRows * lineHeight : Infinity;
493
+ el.style.height = `${Math.min(Math.max(content, min), max) + chrome}px`;
494
+ el.style.overflowY = content > max ? 'auto' : 'hidden';
495
+ }
496
+ onTypedKeyup(event) {
497
+ this.commitOn('keyup', event.target.value, event);
498
+ }
499
+ commitPending(event) {
500
+ const field = this.elementRef.nativeElement.querySelector('textarea');
501
+ if (field) {
502
+ this.commitValue(field.value, event);
503
+ }
504
+ }
505
+ /** Enter types a newline here and submits nothing; Ctrl/Cmd+Enter is the submit chord. */
506
+ commitsOnEnter(event) {
507
+ return event.ctrlKey || event.metaKey;
508
+ }
247
509
  onBlur(event) {
510
+ // Blur commits whatever the mode: a value typed and left must not be discarded because
511
+ // the configured event never fired.
248
512
  this.commitValue(event.target.value, event);
249
513
  this.focused.set(false);
250
514
  this.markAsTouched();
251
515
  }
252
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
253
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextAreaComponent, isStandalone: true, selector: "nxe-text-area", inputs: { placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, heightInput: { classPropertyName: "heightInput", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-textarea" }, providers: [
516
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
517
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextAreaComponent, isStandalone: true, selector: "nxe-text-area", inputs: { placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, heightInput: { classPropertyName: "heightInput", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, autoGrow: { classPropertyName: "autoGrow", publicName: "autoGrow", isSignal: true, isRequired: false, transformFunction: null }, minRows: { classPropertyName: "minRows", publicName: "minRows", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-textarea" }, providers: [
254
518
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextAreaComponent), multi: true },
255
519
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextAreaComponent) },
256
- ], usesInheritance: true, ngImport: i0, template: `
257
- <div class="nxe-editor-container">
258
- <textarea
259
- class="nxe-editor-input nxe-textarea-input"
260
- [value]="displayValue()"
261
- [readOnly]="readOnly()"
262
- [disabled]="disabled()"
263
- [attr.placeholder]="placeholder() || null"
264
- [style.height.px]="height() || null"
265
- (blur)="onBlur($event)"
266
- (focus)="focused.set(true)"></textarea>
267
- </div>
268
- <ng-content />
269
- @if (validationError(); as error) {
270
- @if (focused()) {
271
- <div class="nxe-invalid-message">{{ error }}</div>
272
- }
273
- }
274
- `, isInline: true, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
520
+ ], usesInheritance: true, ngImport: i0, template: `
521
+ <div class="nxe-editor-container">
522
+ <textarea
523
+ class="nxe-editor-input nxe-textarea-input"
524
+ [attr.tabindex]="tabIndex()"
525
+ [id]="controlId()"
526
+ [attr.aria-label]="ariaLabel() || null"
527
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
528
+ [attr.aria-invalid]="invalidState() ? true : null"
529
+ [attr.aria-required]="required() ? true : null"
530
+ [value]="displayValue()"
531
+ [readOnly]="readOnly()"
532
+ [disabled]="disabled()"
533
+ [attr.placeholder]="placeholder() || null"
534
+ [attr.maxlength]="maxLength() ?? null"
535
+ [style.height.px]="autoGrow() ? null : height() || null"
536
+ (input)="onTypedInput($event)"
537
+ (keyup)="onTypedKeyup($event)"
538
+ (blur)="onBlur($event)"
539
+ (focus)="focused.set(true)"></textarea>
540
+ </div>
541
+ @if (maxLength() !== undefined) {
542
+ <!-- The visible count is not read out character by character; the status below speaks
543
+ only once the limit is close. -->
544
+ <div
545
+ class="nxe-textarea-counter"
546
+ aria-hidden="true"
547
+ [class.nxe-textarea-counter-near]="nearLimit()"
548
+ [class.nxe-textarea-counter-over]="remaining() < 0">
549
+ {{ fieldText().length }}/{{ maxLength() }}
550
+ </div>
551
+ <span class="nxe-textarea-counter-status" aria-live="polite" aria-atomic="true">{{ limitStatus() }}</span>
552
+ }
553
+ <ng-content />
554
+ @if (validationError(); as error) {
555
+ @if (focused()) {
556
+ <div class="nxe-invalid-message">{{ error }}</div>
557
+ }
558
+ }
559
+ `, isInline: true, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}nxe-text-area .nxe-textarea-counter{padding:0 var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text-muted, #999);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:1.35715;text-align:end;font-variant-numeric:tabular-nums}nxe-text-area .nxe-textarea-counter-near{color:var(--nxe-ui-text, #333)}nxe-text-area .nxe-textarea-counter-over{color:var(--nxe-ui-invalid, #d9534f)}nxe-text-area .nxe-textarea-counter-status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
275
560
  }
276
561
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, decorators: [{
277
562
  type: Component,
278
- args: [{ selector: 'nxe-text-area', standalone: true, template: `
279
- <div class="nxe-editor-container">
280
- <textarea
281
- class="nxe-editor-input nxe-textarea-input"
282
- [value]="displayValue()"
283
- [readOnly]="readOnly()"
284
- [disabled]="disabled()"
285
- [attr.placeholder]="placeholder() || null"
286
- [style.height.px]="height() || null"
287
- (blur)="onBlur($event)"
288
- (focus)="focused.set(true)"></textarea>
289
- </div>
290
- <ng-content />
291
- @if (validationError(); as error) {
292
- @if (focused()) {
293
- <div class="nxe-invalid-message">{{ error }}</div>
294
- }
295
- }
563
+ args: [{ selector: 'nxe-text-area', standalone: true, template: `
564
+ <div class="nxe-editor-container">
565
+ <textarea
566
+ class="nxe-editor-input nxe-textarea-input"
567
+ [attr.tabindex]="tabIndex()"
568
+ [id]="controlId()"
569
+ [attr.aria-label]="ariaLabel() || null"
570
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
571
+ [attr.aria-invalid]="invalidState() ? true : null"
572
+ [attr.aria-required]="required() ? true : null"
573
+ [value]="displayValue()"
574
+ [readOnly]="readOnly()"
575
+ [disabled]="disabled()"
576
+ [attr.placeholder]="placeholder() || null"
577
+ [attr.maxlength]="maxLength() ?? null"
578
+ [style.height.px]="autoGrow() ? null : height() || null"
579
+ (input)="onTypedInput($event)"
580
+ (keyup)="onTypedKeyup($event)"
581
+ (blur)="onBlur($event)"
582
+ (focus)="focused.set(true)"></textarea>
583
+ </div>
584
+ @if (maxLength() !== undefined) {
585
+ <!-- The visible count is not read out character by character; the status below speaks
586
+ only once the limit is close. -->
587
+ <div
588
+ class="nxe-textarea-counter"
589
+ aria-hidden="true"
590
+ [class.nxe-textarea-counter-near]="nearLimit()"
591
+ [class.nxe-textarea-counter-over]="remaining() < 0">
592
+ {{ fieldText().length }}/{{ maxLength() }}
593
+ </div>
594
+ <span class="nxe-textarea-counter-status" aria-live="polite" aria-atomic="true">{{ limitStatus() }}</span>
595
+ }
596
+ <ng-content />
597
+ @if (validationError(); as error) {
598
+ @if (focused()) {
599
+ <div class="nxe-invalid-message">{{ error }}</div>
600
+ }
601
+ }
296
602
  `, host: {
297
603
  class: 'nxe-widget nxe-editor nxe-textarea',
298
604
  '[class.nxe-focused]': 'focused()',
299
- '[class.nxe-invalid]': '!!validationError()',
605
+ '[class.nxe-invalid]': 'invalidState()',
300
606
  }, providers: [
301
607
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextAreaComponent), multi: true },
302
608
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextAreaComponent) },
303
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}\n"] }]
304
- }], propDecorators: { placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], heightInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
609
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}nxe-text-area .nxe-textarea-counter{padding:0 var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text-muted, #999);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:1.35715;text-align:end;font-variant-numeric:tabular-nums}nxe-text-area .nxe-textarea-counter-near{color:var(--nxe-ui-text, #333)}nxe-text-area .nxe-textarea-counter-over{color:var(--nxe-ui-invalid, #d9534f)}nxe-text-area .nxe-textarea-counter-status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"] }]
610
+ }], ctorParameters: () => [], propDecorators: { placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], heightInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], autoGrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoGrow", required: false }] }], minRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "minRows", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }] } });
305
611
 
306
612
  /**
307
613
  * `nxe-check-box` — drop-in replacement for DevExtreme `dx-check-box`.
308
614
  *
309
- * A boolean signal-CVA editor (extends {@link NxeFormControlBase}) — no tri-state, matching all
310
- * usage. Supports `formControlName`/`[(ngModel)]`/`[(value)]`/`[value]`, `[readOnly]`,
311
- * `[disabled]`, an inline `text` label, `(onValueChanged)` (`{value}`) and `(valueChange)` (raw
312
- * boolean). Provides {@link NXE_VALIDATION_TARGET} for a projected `nxe-validator` (a `required`
615
+ * A signal-CVA editor (extends {@link NxeFormControlBase}) over `true` / `false` / `null`. As in
616
+ * dx, a `null` value is the indeterminate state: the box shows a 12px square and reports
617
+ * `aria-checked="mixed"`. That is what a `new FormControl()` (or a `reset()`) looks like, and what
618
+ * `[value]="null"` looks like; an unbound box starts unchecked, like dx's default `false`. A click
619
+ * or Space always lands on `true` or `false` — there is no `enableThreeStateBehavior` cycling back
620
+ * to `null`.
621
+ *
622
+ * Supports `formControlName`/`[(ngModel)]`/`[(value)]`/`[value]`, `[readOnly]`, `[disabled]`, an
623
+ * inline `text` label, `(onValueChanged)` (`{value}`) and `(valueChange)` (raw value). A read-only
624
+ * box stays focusable (tabindex 0) and still shows hover and focus, as in dx; it just never
625
+ * toggles. Provides {@link NXE_VALIDATION_TARGET} for a projected `nxe-validator` (a `required`
313
626
  * rule is invalid while unchecked, matching DevExtreme's RequiredRule treating `false` as empty).
314
627
  */
315
628
  class NxeCheckBoxComponent extends NxeFormControlBase {
316
629
  /** Inline label rendered next to the box. */
317
630
  text = input(/* @ts-ignore */
318
631
  ...(ngDevMode ? [undefined, { debugName: "text" }] : /* istanbul ignore next */ []));
632
+ /**
633
+ * Whether a value has been handed in at all — by the forms API or a `[value]` binding.
634
+ *
635
+ * The base class starts `value` at `null`, which is also the indeterminate value, so `null`
636
+ * alone cannot tell "given null" from "given nothing". dx can: its unset value is `false`.
637
+ */
638
+ valueProvided = signal(false, /* @ts-ignore */
639
+ ...(ngDevMode ? [{ debugName: "valueProvided" }] : /* istanbul ignore next */ []));
640
+ /** dx `.dx-checkbox-indeterminate`: the value was given, and it is `null`. */
641
+ indeterminate = computed(() => this.valueProvided() && this.value() === null, /* @ts-ignore */
642
+ ...(ngDevMode ? [{ debugName: "indeterminate" }] : /* istanbul ignore next */ []));
643
+ writeValue(value) {
644
+ super.writeValue(value);
645
+ this.valueProvided.set(true);
646
+ }
647
+ ngOnChanges(changes) {
648
+ if (changes['value']) {
649
+ this.valueProvided.set(true);
650
+ }
651
+ }
319
652
  toggle(event) {
320
653
  if (this.disabled() || this.readOnly()) {
321
654
  return;
@@ -328,49 +661,109 @@ class NxeCheckBoxComponent extends NxeFormControlBase {
328
661
  this.toggle(event);
329
662
  }
330
663
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeCheckBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
331
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeCheckBoxComponent, isStandalone: true, selector: "nxe-check-box", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "checkbox" }, listeners: { "click": "toggle($event)", "keydown.space": "onSpace($event)", "blur": "markAsTouched()" }, properties: { "attr.aria-checked": "!!value()", "attr.aria-disabled": "disabled() ? true : null", "attr.aria-readonly": "readOnly() ? true : null", "attr.tabindex": "disabled() || readOnly() ? null : 0", "class.nxe-checkbox-checked": "!!value()", "class.nxe-disabled": "disabled()", "class.nxe-readonly": "readOnly()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-checkbox" }, providers: [
664
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeCheckBoxComponent, isStandalone: true, selector: "nxe-check-box", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "checkbox" }, listeners: { "click": "toggle($event)", "keydown.space": "onSpace($event)", "blur": "markAsTouched()" }, properties: { "attr.aria-checked": "indeterminate() ? 'mixed' : !!value()", "attr.aria-disabled": "disabled() ? true : null", "attr.aria-readonly": "readOnly() ? true : null", "attr.aria-invalid": "invalidState() ? true : null", "attr.aria-required": "required() ? true : null", "attr.id": "inputId() ? controlId() : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledBy() || null", "attr.tabindex": "disabled() ? null : 0", "class.nxe-checkbox-checked": "!!value()", "class.nxe-checkbox-indeterminate": "indeterminate()", "class.nxe-disabled": "disabled()", "class.nxe-readonly": "readOnly()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-checkbox" }, providers: [
332
665
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeCheckBoxComponent), multi: true },
333
666
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeCheckBoxComponent) },
334
- ], usesInheritance: true, ngImport: i0, template: `
335
- <span class="nxe-checkbox-container">
336
- <span class="nxe-checkbox-icon" aria-hidden="true"></span>
337
- @if (text()) {
338
- <span class="nxe-checkbox-text">{{ text() }}</span>
339
- }
340
- </span>
341
- <ng-content />
342
- `, isInline: true, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid #ddd;border-radius:2px;background-color:#fff;transition:border-color .12s ease}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;top:3px;inset-inline-start:7px;width:6px;height:11px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}nxe-check-box:hover .nxe-checkbox-icon{border-color:#265a87}nxe-check-box:focus .nxe-checkbox-icon{border-color:#337ab7}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:#f4f4f4;background-color:#fff}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon{border-color:#d9534f66}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:#d9534f}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
667
+ ], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
668
+ <span class="nxe-checkbox-container">
669
+ <span class="nxe-checkbox-icon" aria-hidden="true"></span>
670
+ @if (text()) {
671
+ <span class="nxe-checkbox-text">{{ text() }}</span>
672
+ }
673
+ </span>
674
+ <ng-content />
675
+ `, isInline: true, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background-color:var(--nxe-ui-surface, #fff)}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:var(--nxe-ui-neutral-80, #f4f4f4);background-color:var(--nxe-ui-surface, #fff)}nxe-check-box:not(.nxe-disabled):hover .nxe-checkbox-icon{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}nxe-check-box:not(.nxe-disabled):focus .nxe-checkbox-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-check-box:not(.nxe-disabled):active .nxe-checkbox-icon{background-color:var(--nxe-ui-checkbox-active-bg, rgba(96, 96, 96, .2))}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon,nxe-check-box.nxe-invalid:hover .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid-faded, rgba(217, 83, 79, .4))}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
343
676
  }
344
677
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeCheckBoxComponent, decorators: [{
345
678
  type: Component,
346
- args: [{ selector: 'nxe-check-box', standalone: true, template: `
347
- <span class="nxe-checkbox-container">
348
- <span class="nxe-checkbox-icon" aria-hidden="true"></span>
349
- @if (text()) {
350
- <span class="nxe-checkbox-text">{{ text() }}</span>
351
- }
352
- </span>
353
- <ng-content />
679
+ args: [{ selector: 'nxe-check-box', standalone: true, template: `
680
+ <span class="nxe-checkbox-container">
681
+ <span class="nxe-checkbox-icon" aria-hidden="true"></span>
682
+ @if (text()) {
683
+ <span class="nxe-checkbox-text">{{ text() }}</span>
684
+ }
685
+ </span>
686
+ <ng-content />
354
687
  `, host: {
355
688
  class: 'nxe-widget nxe-checkbox',
356
689
  role: 'checkbox',
357
- '[attr.aria-checked]': '!!value()',
690
+ '[attr.aria-checked]': "indeterminate() ? 'mixed' : !!value()",
358
691
  '[attr.aria-disabled]': 'disabled() ? true : null',
359
692
  '[attr.aria-readonly]': 'readOnly() ? true : null',
360
- '[attr.tabindex]': 'disabled() || readOnly() ? null : 0',
693
+ '[attr.aria-invalid]': 'invalidState() ? true : null',
694
+ '[attr.aria-required]': 'required() ? true : null',
695
+ // The host IS the control here, so the id and name belong on it rather than on a child.
696
+ '[attr.id]': 'inputId() ? controlId() : null',
697
+ '[attr.aria-label]': 'ariaLabel() || null',
698
+ '[attr.aria-labelledby]': 'ariaLabelledBy() || null',
699
+ // dx keeps a read-only box in the tab order (tabindex 0); only disabled takes it out.
700
+ '[attr.tabindex]': 'disabled() ? null : 0',
361
701
  '[class.nxe-checkbox-checked]': '!!value()',
702
+ '[class.nxe-checkbox-indeterminate]': 'indeterminate()',
362
703
  '[class.nxe-disabled]': 'disabled()',
363
704
  '[class.nxe-readonly]': 'readOnly()',
364
- '[class.nxe-invalid]': '!!validationError()',
705
+ '[class.nxe-invalid]': 'invalidState()',
365
706
  '(click)': 'toggle($event)',
366
707
  '(keydown.space)': 'onSpace($event)',
367
708
  '(blur)': 'markAsTouched()',
368
709
  }, providers: [
369
710
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeCheckBoxComponent), multi: true },
370
711
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeCheckBoxComponent) },
371
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid #ddd;border-radius:2px;background-color:#fff;transition:border-color .12s ease}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;top:3px;inset-inline-start:7px;width:6px;height:11px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}nxe-check-box:hover .nxe-checkbox-icon{border-color:#265a87}nxe-check-box:focus .nxe-checkbox-icon{border-color:#337ab7}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:#f4f4f4;background-color:#fff}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon{border-color:#d9534f66}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:#d9534f}\n"] }]
712
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background-color:var(--nxe-ui-surface, #fff)}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:var(--nxe-ui-neutral-80, #f4f4f4);background-color:var(--nxe-ui-surface, #fff)}nxe-check-box:not(.nxe-disabled):hover .nxe-checkbox-icon{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}nxe-check-box:not(.nxe-disabled):focus .nxe-checkbox-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-check-box:not(.nxe-disabled):active .nxe-checkbox-icon{background-color:var(--nxe-ui-checkbox-active-bg, rgba(96, 96, 96, .2))}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon,nxe-check-box.nxe-invalid:hover .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid-faded, rgba(217, 83, 79, .4))}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"] }]
372
713
  }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }] } });
373
714
 
715
+ /**
716
+ * Decimal-safe stepping for nxe-number-box.
717
+ *
718
+ * `0.1 + 0.2` is `0.30000000000000004` in binary floating point, and a spin button that walks a
719
+ * 0.1 step through that arithmetic shows it within a few clicks. Both operands are scaled to
720
+ * integers by the larger of their decimal places, added as integers, and scaled back, which is
721
+ * exact for every value a person can type.
722
+ */
723
+ /** Decimal places in the shortest decimal form of `value` (`0.1` is 1, `1e-7` is 7, `120` is 0). */
724
+ function decimalPlaces(value) {
725
+ if (!Number.isFinite(value)) {
726
+ return 0;
727
+ }
728
+ const text = String(value);
729
+ const exponent = /e([+-]\d+)$/i.exec(text);
730
+ const mantissa = exponent ? text.slice(0, exponent.index) : text;
731
+ const dot = mantissa.indexOf('.');
732
+ const fraction = dot === -1 ? 0 : mantissa.length - dot - 1;
733
+ return Math.max(0, fraction - (exponent ? Number(exponent[1]) : 0));
734
+ }
735
+ /** `a + b` without binary floating point artefacts. */
736
+ function addDecimal(a, b) {
737
+ const places = Math.max(decimalPlaces(a), decimalPlaces(b));
738
+ if (places === 0 || places > 15) {
739
+ return a + b;
740
+ }
741
+ const factor = Math.pow(10, places);
742
+ return (Math.round(a * factor) + Math.round(b * factor)) / factor;
743
+ }
744
+ /** Clamp to the optional bounds. */
745
+ function clampNumber(value, min, max) {
746
+ let next = value;
747
+ if (min != null && next < min) {
748
+ next = min;
749
+ }
750
+ if (max != null && next > max) {
751
+ next = max;
752
+ }
753
+ return next;
754
+ }
755
+ /**
756
+ * The value after `direction` steps of `step` from `current` (`null` counts as 0, or as the
757
+ * nearest bound when 0 is outside them), clamped to `[min, max]`.
758
+ */
759
+ function stepNumber(current, step, direction, min, max) {
760
+ const base = current ?? clampNumber(0, min, max);
761
+ return clampNumber(addDecimal(base, direction * step), min, max);
762
+ }
763
+
764
+ /** Delay before a held spin button starts repeating, and the repeat interval (ms). */
765
+ const SPIN_REPEAT_DELAY = 400;
766
+ const SPIN_REPEAT_INTERVAL = 70;
374
767
  /** Coerce a static (`min="1"`) or bound (`[min]="0"`) attribute to a number, or undefined. */
375
768
  function toNumber(value) {
376
769
  if (value == null || value === '') {
@@ -389,6 +782,21 @@ function toNumber(value) {
389
782
  *
390
783
  * Supports `formControlName`/`[(ngModel)]`/`[(value)]`/`[value]`, `[readOnly]`, `[min]`/`[max]`,
391
784
  * `format` (string LDML pattern), `(valueChange)` (numeric), and a projected `nxe-validator`.
785
+ *
786
+ * Stepping: `step` (default 1) moves the value by exact decimal arithmetic (`0.1` steps never show
787
+ * `0.30000000000000004`) on ArrowUp/ArrowDown (`largeStep`, default 10 steps, on PageUp/PageDown),
788
+ * and on the optional spin buttons (`showSpinButtons`; held buttons repeat unless
789
+ * `[holdToRepeat]="false"`). Results are clamped to `[min, max]` and rounded to the format precision.
790
+ * The input carries `role="spinbutton"` with `aria-valuenow`/`aria-valuemin`/`aria-valuemax`.
791
+ *
792
+ * `currency` (an ISO 4217 code) shows `IDR 1.351.000` / `USD 1,234.50` (separators from
793
+ * `NXE_NUMBER_SEPARATORS`, so it follows the locale) and takes the currency's minor units as the
794
+ * precision. An explicit `format` still sets the number pattern; `currencyDecimals` overrides the
795
+ * minor units (reference data). Focused, the field holds the bare editable number.
796
+ *
797
+ * `showClearButton` (dx default `false`) adds dx's clear area after the input while the field
798
+ * holds text and is neither read-only nor disabled; it sets the value to `null`, as dx's
799
+ * `clear()` does for a number box, and focuses the input.
392
800
  */
393
801
  class NxeNumberBoxComponent extends NxeFormControlBase {
394
802
  /** LDML format pattern (e.g. `#,##0.00`); empty = raw number. */
@@ -397,12 +805,65 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
397
805
  ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
398
806
  min = input(undefined, { ...(ngDevMode ? { debugName: "min" } : /* istanbul ignore next */ {}), transform: toNumber });
399
807
  max = input(undefined, { ...(ngDevMode ? { debugName: "max" } : /* istanbul ignore next */ {}), transform: toNumber });
808
+ /** dx `showClearButton` — off by default, as in dx; `NXE_DEFAULT_OPTIONS.numberBox` can turn it on app-wide. */
809
+ showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
810
+ showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.numberBox?.showClearButton ?? false, /* @ts-ignore */
811
+ ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
812
+ /** Amount of one step (ArrowUp/ArrowDown, spin buttons). Default 1; zero, negative or invalid means 1. */
813
+ step = input(undefined, { ...(ngDevMode ? { debugName: "step" } : /* istanbul ignore next */ {}), transform: toNumber });
814
+ /** PageUp/PageDown amount. Default 10 steps. */
815
+ largeStep = input(undefined, { ...(ngDevMode ? { debugName: "largeStep" } : /* istanbul ignore next */ {}), transform: toNumber });
816
+ /** Show the increase/decrease buttons after the input. Default off, as in dx. */
817
+ showSpinButtons = input(false, { ...(ngDevMode ? { debugName: "showSpinButtons" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
818
+ /** Keep stepping while a spin button is held down. Default on. */
819
+ holdToRepeat = input(true, { ...(ngDevMode ? { debugName: "holdToRepeat" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
820
+ /** ISO 4217 code: shows `CODE amount` and takes the currency's minor units as precision. */
821
+ currency = input(undefined, /* @ts-ignore */
822
+ ...(ngDevMode ? [{ debugName: "currency" }] : /* istanbul ignore next */ []));
823
+ /** The currency's minor units from reference data; overrides the built-in table. */
824
+ currencyDecimals = input(undefined, { ...(ngDevMode ? { debugName: "currencyDecimals" } : /* istanbul ignore next */ {}), transform: toNumber });
400
825
  focused = signal(false, /* @ts-ignore */
401
826
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
402
- parsedFormat = computed(() => parseNumberFormat(this.format()), /* @ts-ignore */
827
+ currencyCode = computed(() => nxeCurrencyCode(this.currency()), /* @ts-ignore */
828
+ ...(ngDevMode ? [{ debugName: "currencyCode" }] : /* istanbul ignore next */ []));
829
+ /** The explicit `format` input only, so a currency does not inherit the app-wide default pattern. */
830
+ explicitFormat = computed(() => parseNumberFormat(this.formatInput() ?? ''), /* @ts-ignore */
831
+ ...(ngDevMode ? [{ debugName: "explicitFormat" }] : /* istanbul ignore next */ []));
832
+ parsedFormat = computed(() => {
833
+ if (this.currencyCode()) {
834
+ return this.explicitFormat() ?? nxeCurrencyNumberFormat(this.currencyCode(), this.currencyDecimals());
835
+ }
836
+ return parseNumberFormat(this.format());
837
+ }, /* @ts-ignore */
403
838
  ...(ngDevMode ? [{ debugName: "parsedFormat" }] : /* istanbul ignore next */ []));
839
+ stepAmount = computed(() => {
840
+ const step = this.step();
841
+ return step != null && step > 0 ? step : 1;
842
+ }, /* @ts-ignore */
843
+ ...(ngDevMode ? [{ debugName: "stepAmount" }] : /* istanbul ignore next */ []));
844
+ largeStepAmount = computed(() => {
845
+ const large = this.largeStep();
846
+ return large != null && large > 0 ? large : this.stepAmount() * 10;
847
+ }, /* @ts-ignore */
848
+ ...(ngDevMode ? [{ debugName: "largeStepAmount" }] : /* istanbul ignore next */ []));
849
+ fieldRef = viewChild.required('field', /* @ts-ignore */
850
+ ...(ngDevMode ? [{ debugName: "fieldRef" }] : /* istanbul ignore next */ []));
404
851
  /** Locale characters, configured once by the application. Defaults to en-US. */
405
852
  separators = inject(NXE_NUMBER_SEPARATORS);
853
+ /** The blurred, formatted text of the committed value. */
854
+ formattedValue = computed(() => {
855
+ if (this.currencyCode()) {
856
+ return formatCurrency(this.value(), this.currencyCode(), this.separators, {
857
+ decimals: this.currencyDecimals(),
858
+ pattern: this.explicitFormat(),
859
+ });
860
+ }
861
+ return formatNumber(this.value(), this.parsedFormat(), this.separators);
862
+ }, /* @ts-ignore */
863
+ ...(ngDevMode ? [{ debugName: "formattedValue" }] : /* istanbul ignore next */ []));
864
+ /** `aria-valuetext`: the formatted amount, only when a format or currency makes it differ from the number. */
865
+ valueText = computed(() => this.value() == null || (!this.currencyCode() && !this.format()) ? null : this.formattedValue(), /* @ts-ignore */
866
+ ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
406
867
  displayValue = computed(() => {
407
868
  const value = this.value();
408
869
  if (this.focused()) {
@@ -410,11 +871,126 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
410
871
  // a JS-style dot, and parseNumber would read that as a group separator and commit 125.
411
872
  return value == null ? '' : String(value).replace('.', this.separators.decimal);
412
873
  }
413
- return formatNumber(value, this.parsedFormat(), this.separators);
874
+ return this.formattedValue();
414
875
  }, /* @ts-ignore */
415
876
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
877
+ /** The text in the field right now; dx shows the clear button by the field's emptiness. */
878
+ fieldText = linkedSignal(() => this.displayValue(), /* @ts-ignore */
879
+ ...(ngDevMode ? [{ debugName: "fieldText" }] : /* istanbul ignore next */ []));
880
+ clearButtonVisible = computed(() => this.showClearButton() && !this.readOnly() && !this.disabled() && this.fieldText() !== '', /* @ts-ignore */
881
+ ...(ngDevMode ? [{ debugName: "clearButtonVisible" }] : /* istanbul ignore next */ []));
882
+ incrementDisabled = computed(() => {
883
+ const max = this.max();
884
+ return this.readOnly() || this.disabled() || (max != null && (this.value() ?? -Infinity) >= max);
885
+ }, /* @ts-ignore */
886
+ ...(ngDevMode ? [{ debugName: "incrementDisabled" }] : /* istanbul ignore next */ []));
887
+ decrementDisabled = computed(() => {
888
+ const min = this.min();
889
+ return this.readOnly() || this.disabled() || (min != null && (this.value() ?? Infinity) <= min);
890
+ }, /* @ts-ignore */
891
+ ...(ngDevMode ? [{ debugName: "decrementDisabled" }] : /* istanbul ignore next */ []));
892
+ spinDelay;
893
+ spinTimer;
894
+ constructor() {
895
+ super();
896
+ inject(DestroyRef).onDestroy(() => this.stopSpin());
897
+ }
898
+ onKeyDownStep(event) {
899
+ if (event.ctrlKey || event.altKey || event.metaKey || this.readOnly() || this.disabled()) {
900
+ return;
901
+ }
902
+ let amount;
903
+ let direction;
904
+ switch (event.key) {
905
+ case 'ArrowUp':
906
+ [amount, direction] = [this.stepAmount(), 1];
907
+ break;
908
+ case 'ArrowDown':
909
+ [amount, direction] = [this.stepAmount(), -1];
910
+ break;
911
+ case 'PageUp':
912
+ [amount, direction] = [this.largeStepAmount(), 1];
913
+ break;
914
+ case 'PageDown':
915
+ [amount, direction] = [this.largeStepAmount(), -1];
916
+ break;
917
+ default:
918
+ return;
919
+ }
920
+ event.preventDefault();
921
+ this.stepBy(amount, direction, event);
922
+ }
923
+ onSpinPointerDown(event, direction) {
924
+ if (event.button !== 0 || this.readOnly() || this.disabled()) {
925
+ return;
926
+ }
927
+ // Keep focus (and the typed text) in the input.
928
+ event.preventDefault();
929
+ this.stopSpin();
930
+ if (!this.stepBy(this.stepAmount(), direction, event) || !this.holdToRepeat()) {
931
+ return;
932
+ }
933
+ this.spinDelay = setTimeout(() => {
934
+ this.spinTimer = setInterval(() => {
935
+ if (!this.stepBy(this.stepAmount(), direction, event)) {
936
+ this.stopSpin();
937
+ }
938
+ }, SPIN_REPEAT_INTERVAL);
939
+ }, SPIN_REPEAT_DELAY);
940
+ }
941
+ /** Keyboard and assistive-technology activation (`detail` 0); pointer presses stepped on pointerdown. */
942
+ onSpinClick(event, direction) {
943
+ if (event.detail === 0) {
944
+ this.stepBy(this.stepAmount(), direction, event);
945
+ }
946
+ }
947
+ stopSpin() {
948
+ clearTimeout(this.spinDelay);
949
+ clearInterval(this.spinTimer);
950
+ this.spinDelay = this.spinTimer = undefined;
951
+ }
952
+ /** Step the value; returns false when it did not move (a bound, or nothing to do). */
953
+ stepBy(amount, direction, event) {
954
+ const field = this.fieldRef().nativeElement;
955
+ // Typed-but-uncommitted text counts as the current value while the field has focus.
956
+ const current = this.focused() ? parseNumber(field.value, this.separators) : this.value();
957
+ let next = stepNumber(current, amount, direction, this.min(), this.max());
958
+ const fmt = this.parsedFormat();
959
+ if (fmt) {
960
+ next = clampNumber(roundHalfAwayFromZero(next, fmt.maxFrac), this.min(), this.max());
961
+ }
962
+ const moved = next !== current;
963
+ this.commitValue(next, event);
964
+ field.value = this.displayValue();
965
+ this.fieldText.set(field.value);
966
+ return moved;
967
+ }
968
+ onFieldInput(event) {
969
+ this.fieldText.set(event.target.value);
970
+ }
971
+ /**
972
+ * dx `_clearValueHandler`: value to `null`, then focus on the input. The field is emptied by hand
973
+ * too, for typed-but-uncommitted text over a value that was already `null`.
974
+ */
975
+ onClear(event) {
976
+ const field = this.fieldRef().nativeElement;
977
+ this.commitValue(null, event);
978
+ field.value = '';
979
+ this.fieldText.set('');
980
+ if (!this.focused()) {
981
+ field.focus();
982
+ }
983
+ }
984
+ commitPending(event) {
985
+ this.commitText(this.fieldRef().nativeElement.value, event);
986
+ }
416
987
  onBlur(event) {
417
- let num = parseNumber(event.target.value, this.separators);
988
+ this.commitText(event.target.value, event);
989
+ this.focused.set(false);
990
+ this.markAsTouched();
991
+ }
992
+ commitText(text, event) {
993
+ let num = parseNumber(text, this.separators);
418
994
  if (num != null) {
419
995
  const min = this.min();
420
996
  const max = this.max();
@@ -430,64 +1006,192 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
430
1006
  }
431
1007
  }
432
1008
  this.commitValue(num, event);
433
- this.focused.set(false);
434
- this.markAsTouched();
435
1009
  }
436
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
437
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeNumberBoxComponent, isStandalone: true, selector: "nxe-number-box", inputs: { formatInput: { classPropertyName: "formatInput", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-numberbox" }, providers: [
1010
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1011
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeNumberBoxComponent, isStandalone: true, selector: "nxe-number-box", inputs: { formatInput: { classPropertyName: "formatInput", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, largeStep: { classPropertyName: "largeStep", publicName: "largeStep", isSignal: true, isRequired: false, transformFunction: null }, showSpinButtons: { classPropertyName: "showSpinButtons", publicName: "showSpinButtons", isSignal: true, isRequired: false, transformFunction: null }, holdToRepeat: { classPropertyName: "holdToRepeat", publicName: "holdToRepeat", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, currencyDecimals: { classPropertyName: "currencyDecimals", publicName: "currencyDecimals", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-numberbox" }, providers: [
438
1012
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeNumberBoxComponent), multi: true },
439
1013
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeNumberBoxComponent) },
440
- ], usesInheritance: true, ngImport: i0, template: `
441
- <div class="nxe-editor-container">
442
- <input
443
- class="nxe-editor-input nxe-numberbox-input"
444
- type="text"
445
- inputmode="decimal"
446
- [value]="displayValue()"
447
- [readOnly]="readOnly()"
448
- [disabled]="disabled()"
449
- autocomplete="off"
450
- (focus)="focused.set(true)"
451
- (blur)="onBlur($event)" />
452
- </div>
453
- <ng-content />
454
- @if (validationError(); as error) {
455
- @if (focused()) {
456
- <div class="nxe-invalid-message">{{ error }}</div>
457
- }
458
- }
459
- `, isInline: true, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1014
+ ], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
1015
+ <div class="nxe-editor-container">
1016
+ <input
1017
+ #field
1018
+ class="nxe-editor-input nxe-numberbox-input"
1019
+ [attr.tabindex]="tabIndex()"
1020
+ [id]="controlId()"
1021
+ [attr.aria-label]="ariaLabel() || null"
1022
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
1023
+ [attr.aria-invalid]="invalidState() ? true : null"
1024
+ [attr.aria-required]="required() ? true : null"
1025
+ type="text"
1026
+ inputmode="decimal"
1027
+ role="spinbutton"
1028
+ [attr.aria-valuenow]="value() ?? null"
1029
+ [attr.aria-valuemin]="min() ?? null"
1030
+ [attr.aria-valuemax]="max() ?? null"
1031
+ [attr.aria-valuetext]="valueText()"
1032
+ [value]="displayValue()"
1033
+ [readOnly]="readOnly()"
1034
+ [disabled]="disabled()"
1035
+ autocomplete="off"
1036
+ (input)="onFieldInput($event)"
1037
+ (keydown)="onKeyDownStep($event)"
1038
+ (focus)="focused.set(true)"
1039
+ (blur)="onBlur($event)" />
1040
+ @if (clearButtonVisible()) {
1041
+ <span
1042
+ class="nxe-clear-button-area"
1043
+ (mousedown)="$event.preventDefault()"
1044
+ (click)="onClear($event)"
1045
+ aria-hidden="true">
1046
+ <!-- A Ø16 disc with a 2px × cut out (the dropdown editors' clear glyph). -->
1047
+ <svg viewBox="0 0 18 18" focusable="false">
1048
+ <path fill-rule="evenodd" transform="rotate(45 9 9)"
1049
+ d="M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z" />
1050
+ </svg>
1051
+ </span>
1052
+ }
1053
+ @if (showSpinButtons()) {
1054
+ <span class="nxe-spin-container">
1055
+ <!-- Not tab stops: the input is the spinbutton and takes ArrowUp/ArrowDown itself.
1056
+ A pointer press keeps focus in the input; a click with detail 0 is keyboard or AT. -->
1057
+ <button
1058
+ type="button"
1059
+ class="nxe-spin-button nxe-spin-up"
1060
+ tabindex="-1"
1061
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.increase')"
1062
+ [attr.aria-controls]="controlId()"
1063
+ [disabled]="incrementDisabled()"
1064
+ (pointerdown)="onSpinPointerDown($event, 1)"
1065
+ (pointerup)="stopSpin()"
1066
+ (pointerleave)="stopSpin()"
1067
+ (pointercancel)="stopSpin()"
1068
+ (click)="onSpinClick($event, 1)">
1069
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1070
+ <path d="M1 5l4-4 4 4" />
1071
+ </svg>
1072
+ </button>
1073
+ <button
1074
+ type="button"
1075
+ class="nxe-spin-button nxe-spin-down"
1076
+ tabindex="-1"
1077
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.decrease')"
1078
+ [attr.aria-controls]="controlId()"
1079
+ [disabled]="decrementDisabled()"
1080
+ (pointerdown)="onSpinPointerDown($event, -1)"
1081
+ (pointerup)="stopSpin()"
1082
+ (pointerleave)="stopSpin()"
1083
+ (pointercancel)="stopSpin()"
1084
+ (click)="onSpinClick($event, -1)">
1085
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1086
+ <path d="M1 1l4 4 4-4" />
1087
+ </svg>
1088
+ </button>
1089
+ </span>
1090
+ }
1091
+ </div>
1092
+ <ng-content />
1093
+ @if (validationError(); as error) {
1094
+ @if (focused()) {
1095
+ <div class="nxe-invalid-message">{{ error }}</div>
1096
+ }
1097
+ }
1098
+ `, isInline: true, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}nxe-number-box .nxe-spin-container{box-sizing:border-box;flex:0 0 24px;align-self:stretch;display:flex;flex-direction:column;width:24px;min-width:24px}nxe-number-box .nxe-editor-container:has(>.nxe-spin-container){--nxe-invalid-badge-inset: 28px}nxe-number-box .nxe-spin-button{appearance:none;box-sizing:border-box;flex:1 1 50%;display:inline-flex;align-items:center;justify-content:center;margin:0;padding:0;border:0;background:transparent;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-number-box .nxe-spin-button svg{display:block;width:10px;height:6px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}nxe-number-box .nxe-spin-button:hover:not(:disabled){color:var(--nxe-ui-text, #333);background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .05))}nxe-number-box .nxe-spin-button:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color, #5c95c5);outline-offset:-2px}nxe-number-box .nxe-spin-button:disabled{cursor:default;opacity:.4}nxe-number-box.nxe-disabled .nxe-spin-button,nxe-number-box.nxe-readonly .nxe-spin-button{cursor:default}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
460
1099
  }
461
1100
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, decorators: [{
462
1101
  type: Component,
463
- args: [{ selector: 'nxe-number-box', standalone: true, template: `
464
- <div class="nxe-editor-container">
465
- <input
466
- class="nxe-editor-input nxe-numberbox-input"
467
- type="text"
468
- inputmode="decimal"
469
- [value]="displayValue()"
470
- [readOnly]="readOnly()"
471
- [disabled]="disabled()"
472
- autocomplete="off"
473
- (focus)="focused.set(true)"
474
- (blur)="onBlur($event)" />
475
- </div>
476
- <ng-content />
477
- @if (validationError(); as error) {
478
- @if (focused()) {
479
- <div class="nxe-invalid-message">{{ error }}</div>
480
- }
481
- }
1102
+ args: [{ selector: 'nxe-number-box', standalone: true, template: `
1103
+ <div class="nxe-editor-container">
1104
+ <input
1105
+ #field
1106
+ class="nxe-editor-input nxe-numberbox-input"
1107
+ [attr.tabindex]="tabIndex()"
1108
+ [id]="controlId()"
1109
+ [attr.aria-label]="ariaLabel() || null"
1110
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
1111
+ [attr.aria-invalid]="invalidState() ? true : null"
1112
+ [attr.aria-required]="required() ? true : null"
1113
+ type="text"
1114
+ inputmode="decimal"
1115
+ role="spinbutton"
1116
+ [attr.aria-valuenow]="value() ?? null"
1117
+ [attr.aria-valuemin]="min() ?? null"
1118
+ [attr.aria-valuemax]="max() ?? null"
1119
+ [attr.aria-valuetext]="valueText()"
1120
+ [value]="displayValue()"
1121
+ [readOnly]="readOnly()"
1122
+ [disabled]="disabled()"
1123
+ autocomplete="off"
1124
+ (input)="onFieldInput($event)"
1125
+ (keydown)="onKeyDownStep($event)"
1126
+ (focus)="focused.set(true)"
1127
+ (blur)="onBlur($event)" />
1128
+ @if (clearButtonVisible()) {
1129
+ <span
1130
+ class="nxe-clear-button-area"
1131
+ (mousedown)="$event.preventDefault()"
1132
+ (click)="onClear($event)"
1133
+ aria-hidden="true">
1134
+ <!-- A Ø16 disc with a 2px × cut out (the dropdown editors' clear glyph). -->
1135
+ <svg viewBox="0 0 18 18" focusable="false">
1136
+ <path fill-rule="evenodd" transform="rotate(45 9 9)"
1137
+ d="M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z" />
1138
+ </svg>
1139
+ </span>
1140
+ }
1141
+ @if (showSpinButtons()) {
1142
+ <span class="nxe-spin-container">
1143
+ <!-- Not tab stops: the input is the spinbutton and takes ArrowUp/ArrowDown itself.
1144
+ A pointer press keeps focus in the input; a click with detail 0 is keyboard or AT. -->
1145
+ <button
1146
+ type="button"
1147
+ class="nxe-spin-button nxe-spin-up"
1148
+ tabindex="-1"
1149
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.increase')"
1150
+ [attr.aria-controls]="controlId()"
1151
+ [disabled]="incrementDisabled()"
1152
+ (pointerdown)="onSpinPointerDown($event, 1)"
1153
+ (pointerup)="stopSpin()"
1154
+ (pointerleave)="stopSpin()"
1155
+ (pointercancel)="stopSpin()"
1156
+ (click)="onSpinClick($event, 1)">
1157
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1158
+ <path d="M1 5l4-4 4 4" />
1159
+ </svg>
1160
+ </button>
1161
+ <button
1162
+ type="button"
1163
+ class="nxe-spin-button nxe-spin-down"
1164
+ tabindex="-1"
1165
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.decrease')"
1166
+ [attr.aria-controls]="controlId()"
1167
+ [disabled]="decrementDisabled()"
1168
+ (pointerdown)="onSpinPointerDown($event, -1)"
1169
+ (pointerup)="stopSpin()"
1170
+ (pointerleave)="stopSpin()"
1171
+ (pointercancel)="stopSpin()"
1172
+ (click)="onSpinClick($event, -1)">
1173
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1174
+ <path d="M1 1l4 4 4-4" />
1175
+ </svg>
1176
+ </button>
1177
+ </span>
1178
+ }
1179
+ </div>
1180
+ <ng-content />
1181
+ @if (validationError(); as error) {
1182
+ @if (focused()) {
1183
+ <div class="nxe-invalid-message">{{ error }}</div>
1184
+ }
1185
+ }
482
1186
  `, host: {
483
1187
  class: 'nxe-widget nxe-editor nxe-numberbox',
484
1188
  '[class.nxe-focused]': 'focused()',
485
- '[class.nxe-invalid]': '!!validationError()',
1189
+ '[class.nxe-invalid]': 'invalidState()',
486
1190
  }, providers: [
487
1191
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeNumberBoxComponent), multi: true },
488
1192
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeNumberBoxComponent) },
489
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}\n"] }]
490
- }], propDecorators: { formatInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }] } });
1193
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}nxe-number-box .nxe-spin-container{box-sizing:border-box;flex:0 0 24px;align-self:stretch;display:flex;flex-direction:column;width:24px;min-width:24px}nxe-number-box .nxe-editor-container:has(>.nxe-spin-container){--nxe-invalid-badge-inset: 28px}nxe-number-box .nxe-spin-button{appearance:none;box-sizing:border-box;flex:1 1 50%;display:inline-flex;align-items:center;justify-content:center;margin:0;padding:0;border:0;background:transparent;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-number-box .nxe-spin-button svg{display:block;width:10px;height:6px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}nxe-number-box .nxe-spin-button:hover:not(:disabled){color:var(--nxe-ui-text, #333);background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .05))}nxe-number-box .nxe-spin-button:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color, #5c95c5);outline-offset:-2px}nxe-number-box .nxe-spin-button:disabled{cursor:default;opacity:.4}nxe-number-box.nxe-disabled .nxe-spin-button,nxe-number-box.nxe-readonly .nxe-spin-button{cursor:default}\n"] }]
1194
+ }], ctorParameters: () => [], propDecorators: { formatInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], largeStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "largeStep", required: false }] }], showSpinButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSpinButtons", required: false }] }], holdToRepeat: [{ type: i0.Input, args: [{ isSignal: true, alias: "holdToRepeat", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], currencyDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyDecimals", required: false }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
491
1195
 
492
1196
  /** Normalize a `[min]`/`[max]` binding (Date object or serialized string) to a DateTime. */
493
1197
  function toBoundDate(bound) {
@@ -508,6 +1212,15 @@ function toBoundDate(bound) {
508
1212
  * typeable (parsed on blur per `displayFormat`) and opens a CDK-overlay calendar from the icon
509
1213
  * button. Built on Luxon. Default `displayFormat` is `dd-MMM-yyyy`. Hosts a projected
510
1214
  * `nxe-validator` via {@link NXE_VALIDATION_TARGET}.
1215
+ *
1216
+ * `showClearButton` (dx default `false`) adds dx's clear area between the input and the calendar
1217
+ * button while the field holds text and is neither read-only nor disabled; it sets the value to
1218
+ * `null` and focuses the input.
1219
+ *
1220
+ * The button and calendar glyphs are drawn in an 18-unit viewBox (1 unit = 1px) to the ink measured
1221
+ * off dx's `event` (`date`), `eventall` (`datetime`) and `clock` (`time`) glyphs and its chevrons,
1222
+ * each laid out from those metrics in rects, stroked rects and circles, and straight-edged polygons;
1223
+ * the templates state each construction.
511
1224
  */
512
1225
  class NxeDateBoxComponent extends NxeFormControlBase {
513
1226
  overlay = inject(Overlay);
@@ -529,6 +1242,10 @@ class NxeDateBoxComponent extends NxeFormControlBase {
529
1242
  ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
530
1243
  max = input(null, /* @ts-ignore */
531
1244
  ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
1245
+ /** dx `showClearButton` — off by default, as in dx; `NXE_DEFAULT_OPTIONS.dateBox` can turn it on app-wide. */
1246
+ showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
1247
+ showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.dateBox?.showClearButton ?? false, /* @ts-ignore */
1248
+ ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
532
1249
  focused = signal(false, /* @ts-ignore */
533
1250
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
534
1251
  opened = signal(false, /* @ts-ignore */
@@ -538,7 +1255,12 @@ class NxeDateBoxComponent extends NxeFormControlBase {
538
1255
  /** Calendar zoom level; caption clicks zoom out, cell picks zoom back in (dx-calendar parity). */
539
1256
  zoomLevel = signal('month', /* @ts-ignore */
540
1257
  ...(ngDevMode ? [{ debugName: "zoomLevel" }] : /* istanbul ignore next */ []));
541
- weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1258
+ /** Short weekday names for the active locale, Sunday first (the grid below starts on Sunday). */
1259
+ weekdays = computed(() => {
1260
+ const monday = Info.weekdays('short', { locale: this.nxeMessages.locale() });
1261
+ return [monday[6], ...monday.slice(0, 6)];
1262
+ }, /* @ts-ignore */
1263
+ ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
542
1264
  /** The format actually used, from `displayFormat` or the default for this `type`. */
543
1265
  effectiveDisplayFormat = computed(() => {
544
1266
  const stated = this.displayFormat() ?? this.nxeDefaults.dateBox?.displayFormat;
@@ -558,10 +1280,16 @@ class NxeDateBoxComponent extends NxeFormControlBase {
558
1280
  /** dx's default placeholder is '', but orbit's global default is the format itself. */
559
1281
  effectivePlaceholder = computed(() => this.placeholder() ?? this.nxeDefaults.dateBox?.placeholder ?? this.effectiveDisplayFormat(), /* @ts-ignore */
560
1282
  ...(ngDevMode ? [{ debugName: "effectivePlaceholder" }] : /* istanbul ignore next */ []));
561
- selected = computed(() => parseDateValue(this.value()), /* @ts-ignore */
1283
+ /** The value as a Luxon date in the active locale, so `MMM` and `MMMM` follow the language. */
1284
+ selected = computed(() => parseDateValue(this.value())?.setLocale(this.nxeMessages.locale()) ?? null, /* @ts-ignore */
562
1285
  ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
563
1286
  displayValue = computed(() => formatDisplay(this.selected(), this.effectiveDisplayFormat()), /* @ts-ignore */
564
1287
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
1288
+ /** The text in the field right now; dx shows the clear button by the field's emptiness. */
1289
+ fieldText = linkedSignal(() => this.displayValue(), /* @ts-ignore */
1290
+ ...(ngDevMode ? [{ debugName: "fieldText" }] : /* istanbul ignore next */ []));
1291
+ clearButtonVisible = computed(() => this.showClearButton() && !this.readOnly() && !this.disabled() && this.fieldText() !== '', /* @ts-ignore */
1292
+ ...(ngDevMode ? [{ debugName: "clearButtonVisible" }] : /* istanbul ignore next */ []));
565
1293
  weeks = computed(() => buildCalendarWeeks(this.viewMonth()), /* @ts-ignore */
566
1294
  ...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
567
1295
  /** Two-digit hour/minute for the popup's time strip; blank when nothing is selected. */
@@ -575,7 +1303,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
575
1303
  */
576
1304
  textAtFocus = '';
577
1305
  caption = computed(() => {
578
- const view = this.viewMonth();
1306
+ const view = this.viewMonth().setLocale(this.nxeMessages.locale());
579
1307
  switch (this.zoomLevel()) {
580
1308
  case 'month':
581
1309
  return view.toFormat('MMMM yyyy');
@@ -601,7 +1329,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
601
1329
  const min = this.minDate();
602
1330
  const max = this.maxDate();
603
1331
  const span = zoom === 'year' ? { months: 1 } : zoom === 'decade' ? { years: 1 } : { years: 10 };
604
- return buildPeriodCells(zoom, this.viewMonth()).map((cell) => {
1332
+ return buildPeriodCells(zoom, this.viewMonth().setLocale(this.nxeMessages.locale())).map((cell) => {
605
1333
  const end = cell.anchor.plus(span).minus({ days: 1 }).endOf('day');
606
1334
  return {
607
1335
  ...cell,
@@ -670,6 +1398,21 @@ class NxeDateBoxComponent extends NxeFormControlBase {
670
1398
  }
671
1399
  this.opened() ? this.close() : this.open();
672
1400
  }
1401
+ onFieldInput(event) {
1402
+ this.fieldText.set(event.target.value);
1403
+ }
1404
+ /**
1405
+ * dx `_clearValueHandler`: value to `null` (dx's `clear()` for a date box), then focus on the
1406
+ * input. `syncField` repaints the field, so typed-but-uncommitted text over a value that was
1407
+ * already `null` goes too.
1408
+ */
1409
+ onClear(event) {
1410
+ this.commitValue(null, event);
1411
+ this.syncField();
1412
+ if (!this.focused()) {
1413
+ this.fieldRef().nativeElement.focus();
1414
+ }
1415
+ }
673
1416
  onFocus(event) {
674
1417
  this.textAtFocus = event.target.value;
675
1418
  this.focused.set(true);
@@ -697,6 +1440,15 @@ class NxeDateBoxComponent extends NxeFormControlBase {
697
1440
  const hadBaseline = this.focused();
698
1441
  this.focused.set(false);
699
1442
  this.markAsTouched();
1443
+ this.commitTypedText(element, hadBaseline, event);
1444
+ }
1445
+ /** Enter commits what was typed, before a surrounding form submits; nothing typed means nothing to do. */
1446
+ commitPending(event) {
1447
+ if (this.focused()) {
1448
+ this.commitTypedText(this.fieldRef().nativeElement, true, event);
1449
+ }
1450
+ }
1451
+ commitTypedText(element, hadBaseline, event) {
700
1452
  if (hadBaseline && element.value === this.textAtFocus) {
701
1453
  return;
702
1454
  }
@@ -705,7 +1457,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
705
1457
  this.syncField();
706
1458
  return;
707
1459
  }
708
- const dt = parseDisplay(element.value, this.effectiveDisplayFormat(), this.selected());
1460
+ const dt = parseDisplay(element.value, this.effectiveDisplayFormat(), this.selected(), this.nxeMessages.locale());
709
1461
  if (dt) {
710
1462
  this.commitValue(serializeDate(this.clampToBounds(dt)), event);
711
1463
  }
@@ -721,6 +1473,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
721
1473
  const element = this.fieldRef().nativeElement;
722
1474
  element.value = this.displayValue();
723
1475
  this.textAtFocus = element.value;
1476
+ this.fieldText.set(element.value);
724
1477
  }
725
1478
  selectDay(day) {
726
1479
  if (this.isDisabledDay(day)) {
@@ -809,277 +1562,1000 @@ class NxeDateBoxComponent extends NxeFormControlBase {
809
1562
  this.overlayRef?.dispose();
810
1563
  }
811
1564
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
812
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDateBoxComponent, isStandalone: true, selector: "nxe-date-box", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-datebox" }, providers: [
1565
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDateBoxComponent, isStandalone: true, selector: "nxe-date-box", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-datebox" }, providers: [
813
1566
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateBoxComponent), multi: true },
814
1567
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateBoxComponent) },
815
- ], viewQueries: [{ propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
816
- <div class="nxe-editor-container">
817
- <input
818
- #field
819
- class="nxe-editor-input nxe-datebox-input"
820
- type="text"
821
- [value]="displayValue()"
822
- [readOnly]="readOnly()"
823
- [disabled]="disabled()"
824
- [attr.placeholder]="effectivePlaceholder() || null"
825
- autocomplete="off"
826
- (focus)="onFocus($event)"
827
- (blur)="onBlur($event)" />
828
- @if (!readOnly()) {
829
- <span
830
- class="nxe-datebox-button"
831
- [class.nxe-datebox-open]="opened()"
832
- (mousedown)="onButtonMousedown($event)"
833
- aria-hidden="true">
834
- @if (type() === 'datetime' || type() === 'time') {
835
- <svg class="nxe-datebox-icon" viewBox="0 0 24 24">
836
- <path
837
- d="M12 2a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 110-16 8 8 0 010 16zm.5-13H11v6l5.2 3.1.8-1.3-4.5-2.7V7z" />
838
- </svg>
839
- } @else {
840
- <svg class="nxe-datebox-icon" viewBox="0 0 24 24">
841
- <path
842
- d="M19 3h-1V1h-2v2H8V1H6v2H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zm0 16H5V8h14v11z" />
843
- </svg>
844
- }
845
- </span>
846
- }
847
- </div>
848
- <ng-content />
849
- @if (validationError(); as error) {
850
- @if (focused()) {
851
- <div class="nxe-invalid-message">{{ error }}</div>
852
- }
853
- }
854
-
855
- <ng-template #calendarTemplate>
856
- <div class="nxe-calendar" (mousedown)="$event.preventDefault()">
857
- @if (type() !== 'time') {
858
- <div class="nxe-calendar-navigator">
859
- <button type="button" class="nxe-calendar-nav" (click)="navigate(-1)">
860
- <svg class="nxe-calendar-chevron" viewBox="0 0 24 24">
861
- <path d="M15.4 7.4 14 6l-6 6 6 6 1.4-1.4L10.8 12z" />
862
- </svg>
863
- </button>
864
- <button type="button" class="nxe-calendar-caption" (click)="zoomOut()">
865
- {{ caption() }}
866
- </button>
867
- <button type="button" class="nxe-calendar-nav" (click)="navigate(1)">
868
- <svg class="nxe-calendar-chevron" viewBox="0 0 24 24">
869
- <path d="M8.6 16.6 10 18l6-6-6-6-1.4 1.4 4.6 4.6z" />
870
- </svg>
871
- </button>
872
- </div>
873
- @if (zoomLevel() === 'month') {
874
- <div class="nxe-calendar-weekdays">
875
- @for (wd of weekdays; track wd) {
876
- <span>{{ wd }}</span>
877
- }
878
- </div>
879
- <div class="nxe-calendar-grid">
880
- @for (week of weeks(); track $index) {
881
- @for (day of week; track day.toMillis()) {
882
- <button
883
- type="button"
884
- class="nxe-calendar-day"
885
- [class.nxe-calendar-other]="day.month !== viewMonth().month"
886
- [class.nxe-calendar-selected]="isSelected(day)"
887
- [class.nxe-calendar-today]="isToday(day)"
888
- [disabled]="isDisabledDay(day)"
889
- (click)="selectDay(day)">
890
- <span>{{ day.day }}</span>
891
- </button>
892
- }
893
- }
894
- </div>
895
- } @else {
896
- <div
897
- class="nxe-calendar-periods"
898
- [class.nxe-calendar-periods-century]="zoomLevel() === 'century'">
899
- @for (cell of periodCells(); track cell.anchor.toMillis()) {
900
- <button
901
- type="button"
902
- class="nxe-calendar-period"
903
- [class.nxe-calendar-other]="cell.other"
904
- [class.nxe-calendar-today]="cell.isNow"
905
- [disabled]="cell.disabled"
906
- (click)="selectPeriod(cell)">
907
- <span>{{ cell.label }}</span>
908
- </button>
909
- }
910
- </div>
911
- }
912
- }
913
- @if (type() !== 'date') {
914
- <div class="nxe-calendar-time">
915
- <input
916
- class="nxe-calendar-time-part"
917
- type="text"
918
- inputmode="numeric"
919
- maxlength="2"
920
- aria-label="Hour"
921
- [value]="hourText()"
922
- (change)="setTimePart('hour', $event)" />
923
- <span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
924
- <input
925
- class="nxe-calendar-time-part"
926
- type="text"
927
- inputmode="numeric"
928
- maxlength="2"
929
- aria-label="Minute"
930
- [value]="minuteText()"
931
- (change)="setTimePart('minute', $event)" />
932
- <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">Now</button>
933
- <button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
934
- Done
935
- </button>
936
- </div>
937
- }
938
- </div>
939
- </ng-template>
940
- `, isInline: true, styles: ["nxe-date-box .nxe-datebox-button{flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;cursor:pointer;color:#333;border-radius:4px}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 6px 12px #0000002e;color:#333;font-size:12px;-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid #ddd;background:transparent;color:#333;cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:#0000000a}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:12px;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:#333;font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:#333}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:#337ab73d}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:#33333361}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:#33333361}.nxe-calendar-today span{border:2px solid #337ab7}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:#337ab7;color:#fff;box-shadow:0 0 0 2px #337ab780;border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid #ddd}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid #ddd;border-radius:4px;background:#fff;color:#333;font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid #ddd;border-radius:4px;background:transparent;color:#333;font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:#0000000a}.nxe-calendar-time-btn:focus-visible{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1568
+ ], viewQueries: [{ propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
1569
+ <div class="nxe-editor-container">
1570
+ <input
1571
+ #field
1572
+ class="nxe-editor-input nxe-datebox-input"
1573
+ [attr.tabindex]="tabIndex()"
1574
+ [id]="controlId()"
1575
+ [attr.aria-label]="ariaLabel() || null"
1576
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
1577
+ [attr.aria-invalid]="invalidState() ? true : null"
1578
+ [attr.aria-required]="required() ? true : null"
1579
+ type="text"
1580
+ [value]="displayValue()"
1581
+ [readOnly]="readOnly()"
1582
+ [disabled]="disabled()"
1583
+ [attr.placeholder]="effectivePlaceholder() || null"
1584
+ autocomplete="off"
1585
+ (input)="onFieldInput($event)"
1586
+ (focus)="onFocus($event)"
1587
+ (blur)="onBlur($event)" />
1588
+ @if (clearButtonVisible()) {
1589
+ <span
1590
+ class="nxe-clear-button-area"
1591
+ (mousedown)="$event.preventDefault()"
1592
+ (click)="onClear($event)"
1593
+ aria-hidden="true">
1594
+ <!-- A Ø16 disc with a 2px × cut out (the dropdown editors' clear glyph). -->
1595
+ <svg viewBox="0 0 18 18" focusable="false">
1596
+ <path fill-rule="evenodd" transform="rotate(45 9 9)"
1597
+ d="M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z" />
1598
+ </svg>
1599
+ </span>
1600
+ }
1601
+ @if (!readOnly()) {
1602
+ <span
1603
+ class="nxe-datebox-button"
1604
+ [class.nxe-datebox-open]="opened()"
1605
+ (mousedown)="onButtonMousedown($event)"
1606
+ aria-hidden="true">
1607
+ @switch (type()) {
1608
+ @case ('time') {
1609
+ <!-- dx clock, a 16x16 ink box round the box centre: a 2px ring (an r 7
1610
+ circle stroked 2px), a 2x4 hand straight up from the centre, a 1.5x4
1611
+ hand turned 45° down-right, and a Ø2 hub where they meet. -->
1612
+ <svg class="nxe-datebox-icon nxe-datebox-icon-time" viewBox="0 0 18 18" focusable="false">
1613
+ <circle cx="9" cy="9" r="7" fill="none" stroke="currentColor" stroke-width="2" />
1614
+ <rect x="8" y="5" width="2" height="4" />
1615
+ <rect x="9" y="8.25" width="4" height="1.5" transform="rotate(45 9 9)" />
1616
+ <circle cx="9" cy="9" r="1" />
1617
+ </svg>
1618
+ }
1619
+ @case ('datetime') {
1620
+ <!-- dx eventall: the calendar page below, without its day marker. -->
1621
+ <svg class="nxe-datebox-icon nxe-datebox-icon-datetime" viewBox="0 0 18 18" focusable="false">
1622
+ <rect x="3" y="3" width="12" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="2" />
1623
+ <rect x="3" y="3" width="12" height="3" />
1624
+ <rect x="4" y="1" width="2" height="1.5" />
1625
+ <rect x="12" y="1" width="2" height="1.5" />
1626
+ </svg>
1627
+ }
1628
+ @default {
1629
+ <!-- dx event, a 14x15 ink box from 1px below the top of the 18px box
1630
+ (where dx's glyph renders): the page is a 12x12 rect with 1px corners
1631
+ stroked 2px, so 14x14 with 2px corners outside and square inside; a
1632
+ 12x3 bar under its top wall makes the 4px header band; two 2x1.5
1633
+ binder tabs stand 1px proud of it; a 4x4 day marker at (9, 9). -->
1634
+ <svg class="nxe-datebox-icon nxe-datebox-icon-date" viewBox="0 0 18 18" focusable="false">
1635
+ <rect x="3" y="3" width="12" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="2" />
1636
+ <rect x="3" y="3" width="12" height="3" />
1637
+ <rect x="4" y="1" width="2" height="1.5" />
1638
+ <rect x="12" y="1" width="2" height="1.5" />
1639
+ <rect x="9" y="9" width="4" height="4" />
1640
+ </svg>
1641
+ }
1642
+ }
1643
+ </span>
1644
+ }
1645
+ </div>
1646
+ <ng-content />
1647
+ @if (validationError(); as error) {
1648
+ @if (focused()) {
1649
+ <div class="nxe-invalid-message">{{ error }}</div>
1650
+ }
1651
+ }
1652
+
1653
+ <ng-template #calendarTemplate>
1654
+ <div class="nxe-calendar" (mousedown)="$event.preventDefault()">
1655
+ @if (type() !== 'time') {
1656
+ <div class="nxe-calendar-navigator">
1657
+ <button type="button" class="nxe-calendar-nav" (click)="navigate(-1)">
1658
+ <!-- dx chevronleft (8x14 ink, centred in its 18px box): two 45° arms
1659
+ whose outer edges run 7px out and 7px up and down from the point,
1660
+ their ends cut square 1.25px across and down (a 1.77px stroke),
1661
+ meeting 2.5px behind it. That draws 8.25x14, centred by putting the
1662
+ point at x 4.875, and rasterises to dx's 8x14. -->
1663
+ <svg class="nxe-calendar-chevron" viewBox="0 0 18 18" focusable="false">
1664
+ <polygon points="4.875 9 11.875 2 13.125 3.25 7.375 9 13.125 14.75 11.875 16" />
1665
+ </svg>
1666
+ </button>
1667
+ <button type="button" class="nxe-calendar-caption" (click)="zoomOut()">
1668
+ {{ caption() }}
1669
+ </button>
1670
+ <button type="button" class="nxe-calendar-nav" (click)="navigate(1)">
1671
+ <!-- dx chevronright: the same chevron mirrored about x 9. -->
1672
+ <svg class="nxe-calendar-chevron" viewBox="0 0 18 18" focusable="false">
1673
+ <polygon points="13.125 9 6.125 2 4.875 3.25 10.625 9 4.875 14.75 6.125 16" />
1674
+ </svg>
1675
+ </button>
1676
+ </div>
1677
+ @if (zoomLevel() === 'month') {
1678
+ <div class="nxe-calendar-weekdays">
1679
+ @for (wd of weekdays(); track $index) {
1680
+ <span>{{ wd }}</span>
1681
+ }
1682
+ </div>
1683
+ <div class="nxe-calendar-grid">
1684
+ @for (week of weeks(); track $index) {
1685
+ @for (day of week; track day.toMillis()) {
1686
+ <button
1687
+ type="button"
1688
+ class="nxe-calendar-day"
1689
+ [class.nxe-calendar-other]="day.month !== viewMonth().month"
1690
+ [class.nxe-calendar-selected]="isSelected(day)"
1691
+ [class.nxe-calendar-today]="isToday(day)"
1692
+ [disabled]="isDisabledDay(day)"
1693
+ (click)="selectDay(day)">
1694
+ <span>{{ day.day }}</span>
1695
+ </button>
1696
+ }
1697
+ }
1698
+ </div>
1699
+ } @else {
1700
+ <div
1701
+ class="nxe-calendar-periods"
1702
+ [class.nxe-calendar-periods-century]="zoomLevel() === 'century'">
1703
+ @for (cell of periodCells(); track cell.anchor.toMillis()) {
1704
+ <button
1705
+ type="button"
1706
+ class="nxe-calendar-period"
1707
+ [class.nxe-calendar-other]="cell.other"
1708
+ [class.nxe-calendar-today]="cell.isNow"
1709
+ [disabled]="cell.disabled"
1710
+ (click)="selectPeriod(cell)">
1711
+ <span>{{ cell.label }}</span>
1712
+ </button>
1713
+ }
1714
+ </div>
1715
+ }
1716
+ }
1717
+ @if (type() !== 'date') {
1718
+ <div class="nxe-calendar-time">
1719
+ <input
1720
+ class="nxe-calendar-time-part"
1721
+ type="text"
1722
+ inputmode="numeric"
1723
+ maxlength="2"
1724
+ [attr.aria-label]="nxeMessages.t('date.hour')"
1725
+ [value]="hourText()"
1726
+ (change)="setTimePart('hour', $event)" />
1727
+ <span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
1728
+ <input
1729
+ class="nxe-calendar-time-part"
1730
+ type="text"
1731
+ inputmode="numeric"
1732
+ maxlength="2"
1733
+ [attr.aria-label]="nxeMessages.t('date.minute')"
1734
+ [value]="minuteText()"
1735
+ (change)="setTimePart('minute', $event)" />
1736
+ <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">{{ nxeMessages.t('date.now') }}</button>
1737
+ <button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
1738
+ {{ nxeMessages.t('date.done') }}
1739
+ </button>
1740
+ </div>
1741
+ }
1742
+ </div>
1743
+ </ng-template>
1744
+ `, isInline: true, styles: ["nxe-date-box .nxe-datebox-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-box .nxe-datebox-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-box:hover:not(.nxe-disabled) .nxe-datebox-button,nxe-date-box:active:not(.nxe-disabled) .nxe-datebox-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-box .nxe-datebox-button.nxe-datebox-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 72px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:6px;box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .18));color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size, 12px);-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nxe-ui-border, #ddd);background:transparent;color:var(--nxe-ui-text, #333);cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-nav:active,.nxe-calendar-caption:active{background-color:var(--nxe-ui-active-bg, rgba(0, 0, 0, .24))}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:inherit;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:var(--nxe-ui-text, #333);font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:var(--nxe-ui-text, #333)}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:var(--nxe-ui-accent-tint, rgba(51, 122, 183, .24))}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-today span{border:2px solid var(--nxe-ui-accent, #337ab7)}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5));border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid var(--nxe-ui-border, #ddd)}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:transparent;color:var(--nxe-ui-text, #333);font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-time-btn:focus-visible{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
941
1745
  }
942
1746
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateBoxComponent, decorators: [{
943
1747
  type: Component,
944
- args: [{ selector: 'nxe-date-box', standalone: true, template: `
945
- <div class="nxe-editor-container">
946
- <input
947
- #field
948
- class="nxe-editor-input nxe-datebox-input"
949
- type="text"
950
- [value]="displayValue()"
951
- [readOnly]="readOnly()"
952
- [disabled]="disabled()"
953
- [attr.placeholder]="effectivePlaceholder() || null"
954
- autocomplete="off"
955
- (focus)="onFocus($event)"
956
- (blur)="onBlur($event)" />
957
- @if (!readOnly()) {
958
- <span
959
- class="nxe-datebox-button"
960
- [class.nxe-datebox-open]="opened()"
961
- (mousedown)="onButtonMousedown($event)"
962
- aria-hidden="true">
963
- @if (type() === 'datetime' || type() === 'time') {
964
- <svg class="nxe-datebox-icon" viewBox="0 0 24 24">
965
- <path
966
- d="M12 2a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 110-16 8 8 0 010 16zm.5-13H11v6l5.2 3.1.8-1.3-4.5-2.7V7z" />
967
- </svg>
968
- } @else {
969
- <svg class="nxe-datebox-icon" viewBox="0 0 24 24">
970
- <path
971
- d="M19 3h-1V1h-2v2H8V1H6v2H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zm0 16H5V8h14v11z" />
972
- </svg>
973
- }
974
- </span>
975
- }
976
- </div>
977
- <ng-content />
978
- @if (validationError(); as error) {
979
- @if (focused()) {
980
- <div class="nxe-invalid-message">{{ error }}</div>
981
- }
982
- }
983
-
984
- <ng-template #calendarTemplate>
985
- <div class="nxe-calendar" (mousedown)="$event.preventDefault()">
986
- @if (type() !== 'time') {
987
- <div class="nxe-calendar-navigator">
988
- <button type="button" class="nxe-calendar-nav" (click)="navigate(-1)">
989
- <svg class="nxe-calendar-chevron" viewBox="0 0 24 24">
990
- <path d="M15.4 7.4 14 6l-6 6 6 6 1.4-1.4L10.8 12z" />
991
- </svg>
992
- </button>
993
- <button type="button" class="nxe-calendar-caption" (click)="zoomOut()">
994
- {{ caption() }}
995
- </button>
996
- <button type="button" class="nxe-calendar-nav" (click)="navigate(1)">
997
- <svg class="nxe-calendar-chevron" viewBox="0 0 24 24">
998
- <path d="M8.6 16.6 10 18l6-6-6-6-1.4 1.4 4.6 4.6z" />
999
- </svg>
1000
- </button>
1001
- </div>
1002
- @if (zoomLevel() === 'month') {
1003
- <div class="nxe-calendar-weekdays">
1004
- @for (wd of weekdays; track wd) {
1005
- <span>{{ wd }}</span>
1006
- }
1007
- </div>
1008
- <div class="nxe-calendar-grid">
1009
- @for (week of weeks(); track $index) {
1010
- @for (day of week; track day.toMillis()) {
1011
- <button
1012
- type="button"
1013
- class="nxe-calendar-day"
1014
- [class.nxe-calendar-other]="day.month !== viewMonth().month"
1015
- [class.nxe-calendar-selected]="isSelected(day)"
1016
- [class.nxe-calendar-today]="isToday(day)"
1017
- [disabled]="isDisabledDay(day)"
1018
- (click)="selectDay(day)">
1019
- <span>{{ day.day }}</span>
1020
- </button>
1021
- }
1022
- }
1023
- </div>
1024
- } @else {
1025
- <div
1026
- class="nxe-calendar-periods"
1027
- [class.nxe-calendar-periods-century]="zoomLevel() === 'century'">
1028
- @for (cell of periodCells(); track cell.anchor.toMillis()) {
1029
- <button
1030
- type="button"
1031
- class="nxe-calendar-period"
1032
- [class.nxe-calendar-other]="cell.other"
1033
- [class.nxe-calendar-today]="cell.isNow"
1034
- [disabled]="cell.disabled"
1035
- (click)="selectPeriod(cell)">
1036
- <span>{{ cell.label }}</span>
1037
- </button>
1038
- }
1039
- </div>
1040
- }
1041
- }
1042
- @if (type() !== 'date') {
1043
- <div class="nxe-calendar-time">
1044
- <input
1045
- class="nxe-calendar-time-part"
1046
- type="text"
1047
- inputmode="numeric"
1048
- maxlength="2"
1049
- aria-label="Hour"
1050
- [value]="hourText()"
1051
- (change)="setTimePart('hour', $event)" />
1052
- <span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
1053
- <input
1054
- class="nxe-calendar-time-part"
1055
- type="text"
1056
- inputmode="numeric"
1057
- maxlength="2"
1058
- aria-label="Minute"
1059
- [value]="minuteText()"
1060
- (change)="setTimePart('minute', $event)" />
1061
- <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">Now</button>
1062
- <button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
1063
- Done
1064
- </button>
1065
- </div>
1066
- }
1067
- </div>
1068
- </ng-template>
1748
+ args: [{ selector: 'nxe-date-box', standalone: true, template: `
1749
+ <div class="nxe-editor-container">
1750
+ <input
1751
+ #field
1752
+ class="nxe-editor-input nxe-datebox-input"
1753
+ [attr.tabindex]="tabIndex()"
1754
+ [id]="controlId()"
1755
+ [attr.aria-label]="ariaLabel() || null"
1756
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
1757
+ [attr.aria-invalid]="invalidState() ? true : null"
1758
+ [attr.aria-required]="required() ? true : null"
1759
+ type="text"
1760
+ [value]="displayValue()"
1761
+ [readOnly]="readOnly()"
1762
+ [disabled]="disabled()"
1763
+ [attr.placeholder]="effectivePlaceholder() || null"
1764
+ autocomplete="off"
1765
+ (input)="onFieldInput($event)"
1766
+ (focus)="onFocus($event)"
1767
+ (blur)="onBlur($event)" />
1768
+ @if (clearButtonVisible()) {
1769
+ <span
1770
+ class="nxe-clear-button-area"
1771
+ (mousedown)="$event.preventDefault()"
1772
+ (click)="onClear($event)"
1773
+ aria-hidden="true">
1774
+ <!-- A Ø16 disc with a 2px × cut out (the dropdown editors' clear glyph). -->
1775
+ <svg viewBox="0 0 18 18" focusable="false">
1776
+ <path fill-rule="evenodd" transform="rotate(45 9 9)"
1777
+ d="M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z" />
1778
+ </svg>
1779
+ </span>
1780
+ }
1781
+ @if (!readOnly()) {
1782
+ <span
1783
+ class="nxe-datebox-button"
1784
+ [class.nxe-datebox-open]="opened()"
1785
+ (mousedown)="onButtonMousedown($event)"
1786
+ aria-hidden="true">
1787
+ @switch (type()) {
1788
+ @case ('time') {
1789
+ <!-- dx clock, a 16x16 ink box round the box centre: a 2px ring (an r 7
1790
+ circle stroked 2px), a 2x4 hand straight up from the centre, a 1.5x4
1791
+ hand turned 45° down-right, and a Ø2 hub where they meet. -->
1792
+ <svg class="nxe-datebox-icon nxe-datebox-icon-time" viewBox="0 0 18 18" focusable="false">
1793
+ <circle cx="9" cy="9" r="7" fill="none" stroke="currentColor" stroke-width="2" />
1794
+ <rect x="8" y="5" width="2" height="4" />
1795
+ <rect x="9" y="8.25" width="4" height="1.5" transform="rotate(45 9 9)" />
1796
+ <circle cx="9" cy="9" r="1" />
1797
+ </svg>
1798
+ }
1799
+ @case ('datetime') {
1800
+ <!-- dx eventall: the calendar page below, without its day marker. -->
1801
+ <svg class="nxe-datebox-icon nxe-datebox-icon-datetime" viewBox="0 0 18 18" focusable="false">
1802
+ <rect x="3" y="3" width="12" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="2" />
1803
+ <rect x="3" y="3" width="12" height="3" />
1804
+ <rect x="4" y="1" width="2" height="1.5" />
1805
+ <rect x="12" y="1" width="2" height="1.5" />
1806
+ </svg>
1807
+ }
1808
+ @default {
1809
+ <!-- dx event, a 14x15 ink box from 1px below the top of the 18px box
1810
+ (where dx's glyph renders): the page is a 12x12 rect with 1px corners
1811
+ stroked 2px, so 14x14 with 2px corners outside and square inside; a
1812
+ 12x3 bar under its top wall makes the 4px header band; two 2x1.5
1813
+ binder tabs stand 1px proud of it; a 4x4 day marker at (9, 9). -->
1814
+ <svg class="nxe-datebox-icon nxe-datebox-icon-date" viewBox="0 0 18 18" focusable="false">
1815
+ <rect x="3" y="3" width="12" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="2" />
1816
+ <rect x="3" y="3" width="12" height="3" />
1817
+ <rect x="4" y="1" width="2" height="1.5" />
1818
+ <rect x="12" y="1" width="2" height="1.5" />
1819
+ <rect x="9" y="9" width="4" height="4" />
1820
+ </svg>
1821
+ }
1822
+ }
1823
+ </span>
1824
+ }
1825
+ </div>
1826
+ <ng-content />
1827
+ @if (validationError(); as error) {
1828
+ @if (focused()) {
1829
+ <div class="nxe-invalid-message">{{ error }}</div>
1830
+ }
1831
+ }
1832
+
1833
+ <ng-template #calendarTemplate>
1834
+ <div class="nxe-calendar" (mousedown)="$event.preventDefault()">
1835
+ @if (type() !== 'time') {
1836
+ <div class="nxe-calendar-navigator">
1837
+ <button type="button" class="nxe-calendar-nav" (click)="navigate(-1)">
1838
+ <!-- dx chevronleft (8x14 ink, centred in its 18px box): two 45° arms
1839
+ whose outer edges run 7px out and 7px up and down from the point,
1840
+ their ends cut square 1.25px across and down (a 1.77px stroke),
1841
+ meeting 2.5px behind it. That draws 8.25x14, centred by putting the
1842
+ point at x 4.875, and rasterises to dx's 8x14. -->
1843
+ <svg class="nxe-calendar-chevron" viewBox="0 0 18 18" focusable="false">
1844
+ <polygon points="4.875 9 11.875 2 13.125 3.25 7.375 9 13.125 14.75 11.875 16" />
1845
+ </svg>
1846
+ </button>
1847
+ <button type="button" class="nxe-calendar-caption" (click)="zoomOut()">
1848
+ {{ caption() }}
1849
+ </button>
1850
+ <button type="button" class="nxe-calendar-nav" (click)="navigate(1)">
1851
+ <!-- dx chevronright: the same chevron mirrored about x 9. -->
1852
+ <svg class="nxe-calendar-chevron" viewBox="0 0 18 18" focusable="false">
1853
+ <polygon points="13.125 9 6.125 2 4.875 3.25 10.625 9 4.875 14.75 6.125 16" />
1854
+ </svg>
1855
+ </button>
1856
+ </div>
1857
+ @if (zoomLevel() === 'month') {
1858
+ <div class="nxe-calendar-weekdays">
1859
+ @for (wd of weekdays(); track $index) {
1860
+ <span>{{ wd }}</span>
1861
+ }
1862
+ </div>
1863
+ <div class="nxe-calendar-grid">
1864
+ @for (week of weeks(); track $index) {
1865
+ @for (day of week; track day.toMillis()) {
1866
+ <button
1867
+ type="button"
1868
+ class="nxe-calendar-day"
1869
+ [class.nxe-calendar-other]="day.month !== viewMonth().month"
1870
+ [class.nxe-calendar-selected]="isSelected(day)"
1871
+ [class.nxe-calendar-today]="isToday(day)"
1872
+ [disabled]="isDisabledDay(day)"
1873
+ (click)="selectDay(day)">
1874
+ <span>{{ day.day }}</span>
1875
+ </button>
1876
+ }
1877
+ }
1878
+ </div>
1879
+ } @else {
1880
+ <div
1881
+ class="nxe-calendar-periods"
1882
+ [class.nxe-calendar-periods-century]="zoomLevel() === 'century'">
1883
+ @for (cell of periodCells(); track cell.anchor.toMillis()) {
1884
+ <button
1885
+ type="button"
1886
+ class="nxe-calendar-period"
1887
+ [class.nxe-calendar-other]="cell.other"
1888
+ [class.nxe-calendar-today]="cell.isNow"
1889
+ [disabled]="cell.disabled"
1890
+ (click)="selectPeriod(cell)">
1891
+ <span>{{ cell.label }}</span>
1892
+ </button>
1893
+ }
1894
+ </div>
1895
+ }
1896
+ }
1897
+ @if (type() !== 'date') {
1898
+ <div class="nxe-calendar-time">
1899
+ <input
1900
+ class="nxe-calendar-time-part"
1901
+ type="text"
1902
+ inputmode="numeric"
1903
+ maxlength="2"
1904
+ [attr.aria-label]="nxeMessages.t('date.hour')"
1905
+ [value]="hourText()"
1906
+ (change)="setTimePart('hour', $event)" />
1907
+ <span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
1908
+ <input
1909
+ class="nxe-calendar-time-part"
1910
+ type="text"
1911
+ inputmode="numeric"
1912
+ maxlength="2"
1913
+ [attr.aria-label]="nxeMessages.t('date.minute')"
1914
+ [value]="minuteText()"
1915
+ (change)="setTimePart('minute', $event)" />
1916
+ <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">{{ nxeMessages.t('date.now') }}</button>
1917
+ <button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
1918
+ {{ nxeMessages.t('date.done') }}
1919
+ </button>
1920
+ </div>
1921
+ }
1922
+ </div>
1923
+ </ng-template>
1069
1924
  `, host: {
1070
1925
  class: 'nxe-widget nxe-editor nxe-datebox',
1071
1926
  '[class.nxe-focused]': 'focused()',
1072
- '[class.nxe-invalid]': '!!validationError()',
1927
+ '[class.nxe-invalid]': 'invalidState()',
1073
1928
  }, providers: [
1074
1929
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateBoxComponent), multi: true },
1075
1930
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateBoxComponent) },
1076
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-date-box .nxe-datebox-button{flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;cursor:pointer;color:#333;border-radius:4px}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 6px 12px #0000002e;color:#333;font-size:12px;-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid #ddd;background:transparent;color:#333;cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:#0000000a}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:12px;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:#333;font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:#333}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:#337ab73d}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:#33333361}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:#33333361}.nxe-calendar-today span{border:2px solid #337ab7}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:#337ab7;color:#fff;box-shadow:0 0 0 2px #337ab780;border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid #ddd}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid #ddd;border-radius:4px;background:#fff;color:#333;font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid #ddd;border-radius:4px;background:transparent;color:#333;font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:#0000000a}.nxe-calendar-time-btn:focus-visible{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"] }]
1077
- }], propDecorators: { calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }] } });
1931
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-date-box .nxe-datebox-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-box .nxe-datebox-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-box:hover:not(.nxe-disabled) .nxe-datebox-button,nxe-date-box:active:not(.nxe-disabled) .nxe-datebox-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-box .nxe-datebox-button.nxe-datebox-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 72px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:6px;box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .18));color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size, 12px);-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nxe-ui-border, #ddd);background:transparent;color:var(--nxe-ui-text, #333);cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-nav:active,.nxe-calendar-caption:active{background-color:var(--nxe-ui-active-bg, rgba(0, 0, 0, .24))}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:inherit;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:var(--nxe-ui-text, #333);font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:var(--nxe-ui-text, #333)}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:var(--nxe-ui-accent-tint, rgba(51, 122, 183, .24))}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-today span{border:2px solid var(--nxe-ui-accent, #337ab7)}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5));border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid var(--nxe-ui-border, #ddd)}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:transparent;color:var(--nxe-ui-text, #333);font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-time-btn:focus-visible{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"] }]
1932
+ }], propDecorators: { calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }] } });
1933
+
1934
+ /**
1935
+ * The clock the box reads for "today": the highlighted day, where the popup opens and what the
1936
+ * built-in presets count back from. Provide a fixed one in a test:
1937
+ *
1938
+ * ```ts
1939
+ * { provide: NXE_DATE_RANGE_NOW, useValue: () => DateTime.fromISO('2026-10-07T09:30:00') }
1940
+ * ```
1941
+ */
1942
+ const NXE_DATE_RANGE_NOW = new InjectionToken('NXE_DATE_RANGE_NOW', {
1943
+ providedIn: 'root',
1944
+ factory: () => () => DateTime.now(),
1945
+ });
1946
+ /** Default `displayFormat`: `1 Okt 2026` in Indonesian, `1 Oct 2026` in English. */
1947
+ const NXE_DATE_RANGE_DISPLAY_FORMAT = 'd MMM yyyy';
1948
+ /** Default `valueFormat`: date-only. */
1949
+ const NXE_DATE_RANGE_VALUE_FORMAT = 'yyyy-MM-dd';
1950
+ /**
1951
+ * The built-in presets. Each call returns a fresh entry whose label comes from the message bundle
1952
+ * and whose range counts from the box's clock. Weeks are not offered: the first day of the week is
1953
+ * a locale question this library does not decide.
1954
+ */
1955
+ const nxeDateRangePresets = {
1956
+ today: () => ({
1957
+ labelKey: 'dateRange.preset.today',
1958
+ range: (now) => ({ start: now.startOf('day'), end: now.startOf('day') }),
1959
+ }),
1960
+ yesterday: () => ({
1961
+ labelKey: 'dateRange.preset.yesterday',
1962
+ range: (now) => {
1963
+ const day = now.startOf('day').minus({ days: 1 });
1964
+ return { start: day, end: day };
1965
+ },
1966
+ }),
1967
+ /** Today and the six days before it. */
1968
+ last7Days: () => ({
1969
+ labelKey: 'dateRange.preset.last7Days',
1970
+ range: (now) => ({ start: now.startOf('day').minus({ days: 6 }), end: now.startOf('day') }),
1971
+ }),
1972
+ /** Today and the 29 days before it. */
1973
+ last30Days: () => ({
1974
+ labelKey: 'dateRange.preset.last30Days',
1975
+ range: (now) => ({ start: now.startOf('day').minus({ days: 29 }), end: now.startOf('day') }),
1976
+ }),
1977
+ /** The first of the month to its last day (not to today). */
1978
+ thisMonth: () => ({
1979
+ labelKey: 'dateRange.preset.thisMonth',
1980
+ range: (now) => ({ start: now.startOf('month'), end: now.endOf('month').startOf('day') }),
1981
+ }),
1982
+ lastMonth: () => ({
1983
+ labelKey: 'dateRange.preset.lastMonth',
1984
+ range: (now) => {
1985
+ const last = now.minus({ months: 1 });
1986
+ return { start: last.startOf('month'), end: last.endOf('month').startOf('day') };
1987
+ },
1988
+ }),
1989
+ /** 1 January to 31 December. */
1990
+ thisYear: () => ({
1991
+ labelKey: 'dateRange.preset.thisYear',
1992
+ range: (now) => ({ start: now.startOf('year'), end: now.endOf('year').startOf('day') }),
1993
+ }),
1994
+ /** All seven, in the order above. */
1995
+ all: () => [
1996
+ nxeDateRangePresets.today(),
1997
+ nxeDateRangePresets.yesterday(),
1998
+ nxeDateRangePresets.last7Days(),
1999
+ nxeDateRangePresets.last30Days(),
2000
+ nxeDateRangePresets.thisMonth(),
2001
+ nxeDateRangePresets.lastMonth(),
2002
+ nxeDateRangePresets.thisYear(),
2003
+ ],
2004
+ };
2005
+
2006
+ /** Below this width the popup shows one month and stacks the presets above it. */
2007
+ const NARROW_QUERY = '(max-width: 719.98px)';
2008
+ let rangeBoxSequence = 0;
2009
+ function toBound(bound) {
2010
+ if (!bound) {
2011
+ return null;
2012
+ }
2013
+ return parseDateValue(bound)?.startOf('day') ?? null;
2014
+ }
2015
+ function dayKey(dt) {
2016
+ return dt.toFormat('yyyy-MM-dd');
2017
+ }
2018
+ /**
2019
+ * `nxe-date-range-box` picks a start and an end date in one field.
2020
+ *
2021
+ * ```html
2022
+ * <nxe-form-field label="Tanggal PO">
2023
+ * <nxe-date-range-box formControlName="poDate" [presets]="presets" min="2020-01-01" />
2024
+ * </nxe-form-field>
2025
+ * ```
2026
+ *
2027
+ * The value is `{ start, end }`, each a string in `valueFormat` (`yyyy-MM-dd` by default; with a time
2028
+ * format the start is the first second of its day and the end the last), `null` while that end is
2029
+ * empty, and `null` as a whole when both are. The field shows `1 Okt 2026 - 31 Okt 2026` and opens
2030
+ * a popup: two months side by side (one under 720px), a quick-range list, and a typed input for each
2031
+ * end. The first click picks the start, the second the end; the days in between preview under the
2032
+ * pointer, and a second click before the first swaps the two.
2033
+ *
2034
+ * Month and weekday names follow the active language of `NxeMessageService`. The field is a
2035
+ * read-only combobox, so a label, `aria-invalid` and `aria-describedby` from `nxe-form-field` reach
2036
+ * it; the popup is a dialog with the days in an ARIA grid (arrows, Home and End by week, PageUp and
2037
+ * PageDown by month, Enter or Space to pick, Esc closes and returns focus to the field).
2038
+ */
2039
+ class NxeDateRangeBoxComponent extends NxeFormControlBase {
2040
+ overlay = inject(Overlay);
2041
+ viewContainerRef = inject(ViewContainerRef);
2042
+ injector = inject(Injector);
2043
+ clock = inject(NXE_DATE_RANGE_NOW);
2044
+ breakpoints = inject(BreakpointObserver);
2045
+ popupTemplate = viewChild.required('popupTemplate', /* @ts-ignore */
2046
+ ...(ngDevMode ? [{ debugName: "popupTemplate" }] : /* istanbul ignore next */ []));
2047
+ fieldRef = viewChild.required('field', /* @ts-ignore */
2048
+ ...(ngDevMode ? [{ debugName: "fieldRef" }] : /* istanbul ignore next */ []));
2049
+ /** How the dates read in the field and in the typed inputs: a Luxon format. */
2050
+ displayFormat = input(NXE_DATE_RANGE_DISPLAY_FORMAT, /* @ts-ignore */
2051
+ ...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
2052
+ /** How each end is written to the value. Date-only unless it carries time tokens. */
2053
+ valueFormat = input(NXE_DATE_RANGE_VALUE_FORMAT, /* @ts-ignore */
2054
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
2055
+ /** Days before this are disabled, and a typed or preset date before it moves to it. */
2056
+ min = input(null, /* @ts-ignore */
2057
+ ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
2058
+ /** Days after this are disabled, and a typed or preset date after it moves to it. */
2059
+ max = input(null, /* @ts-ignore */
2060
+ ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
2061
+ /** The quick-range list. Empty draws none. See `nxeDateRangePresets` for the built-in ones. */
2062
+ presets = input([], /* @ts-ignore */
2063
+ ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
2064
+ /** Shown in the field and the start input while the start is empty. Defaults to the message bundle's. */
2065
+ startPlaceholder = input(undefined, /* @ts-ignore */
2066
+ ...(ngDevMode ? [{ debugName: "startPlaceholder" }] : /* istanbul ignore next */ []));
2067
+ /** Shown in the field and the end input while the end is empty. Defaults to the message bundle's. */
2068
+ endPlaceholder = input(undefined, /* @ts-ignore */
2069
+ ...(ngDevMode ? [{ debugName: "endPlaceholder" }] : /* istanbul ignore next */ []));
2070
+ /** The clear area in the field. On by default: a filter is cleared more often than a form field. */
2071
+ showClearButton = input(true, { ...(ngDevMode ? { debugName: "showClearButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2072
+ focused = signal(false, /* @ts-ignore */
2073
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
2074
+ /** Whether the popup is showing. */
2075
+ opened = signal(false, /* @ts-ignore */
2076
+ ...(ngDevMode ? [{ debugName: "opened" }] : /* istanbul ignore next */ []));
2077
+ uid = `nxe-daterange-${++rangeBoxSequence}`;
2078
+ popupId = `${this.uid}-popup`;
2079
+ isNarrow = toSignal(this.breakpoints.observe(NARROW_QUERY).pipe(map((state) => state.matches)), {
2080
+ initialValue: this.breakpoints.isMatched(NARROW_QUERY),
2081
+ });
2082
+ monthsShown = computed(() => (this.isNarrow() ? 1 : 2), /* @ts-ignore */
2083
+ ...(ngDevMode ? [{ debugName: "monthsShown" }] : /* istanbul ignore next */ []));
2084
+ locale = computed(() => this.nxeMessages.locale(), /* @ts-ignore */
2085
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
2086
+ minDate = computed(() => toBound(this.min()), /* @ts-ignore */
2087
+ ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
2088
+ maxDate = computed(() => toBound(this.max()), /* @ts-ignore */
2089
+ ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
2090
+ /** The committed value as days in the active locale. */
2091
+ range = computed(() => {
2092
+ const value = this.value();
2093
+ const locale = this.locale();
2094
+ return {
2095
+ start: parseDateValue(value?.start)?.startOf('day').setLocale(locale) ?? null,
2096
+ end: parseDateValue(value?.end)?.startOf('day').setLocale(locale) ?? null,
2097
+ };
2098
+ }, /* @ts-ignore */
2099
+ ...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
2100
+ startText = computed(() => formatDisplay(this.range().start, this.displayFormat(), this.locale()), /* @ts-ignore */
2101
+ ...(ngDevMode ? [{ debugName: "startText" }] : /* istanbul ignore next */ []));
2102
+ endText = computed(() => formatDisplay(this.range().end, this.displayFormat(), this.locale()), /* @ts-ignore */
2103
+ ...(ngDevMode ? [{ debugName: "endText" }] : /* istanbul ignore next */ []));
2104
+ startHint = computed(() => this.startPlaceholder() ?? this.nxeMessages.t('dateRange.startPlaceholder'), /* @ts-ignore */
2105
+ ...(ngDevMode ? [{ debugName: "startHint" }] : /* istanbul ignore next */ []));
2106
+ endHint = computed(() => this.endPlaceholder() ?? this.nxeMessages.t('dateRange.endPlaceholder'), /* @ts-ignore */
2107
+ ...(ngDevMode ? [{ debugName: "endHint" }] : /* istanbul ignore next */ []));
2108
+ /** `1 Okt 2026 - 31 Okt 2026`; an empty end leaves the dash hanging so a half-set range reads as one. */
2109
+ displayValue = computed(() => {
2110
+ const start = this.startText();
2111
+ const end = this.endText();
2112
+ return start || end ? `${start} – ${end}`.trim() : '';
2113
+ }, /* @ts-ignore */
2114
+ ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
2115
+ fieldPlaceholder = computed(() => `${this.startHint()} – ${this.endHint()}`, /* @ts-ignore */
2116
+ ...(ngDevMode ? [{ debugName: "fieldPlaceholder" }] : /* istanbul ignore next */ []));
2117
+ clearButtonVisible = computed(() => this.showClearButton() && !this.readOnly() && !this.disabled() && this.displayValue() !== '', /* @ts-ignore */
2118
+ ...(ngDevMode ? [{ debugName: "clearButtonVisible" }] : /* istanbul ignore next */ []));
2119
+ // --- popup state ---
2120
+ /** The first click of a range in progress; null when none is. */
2121
+ draftStart = signal(null, /* @ts-ignore */
2122
+ ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
2123
+ /** The day under the pointer or the keyboard while a range is in progress. */
2124
+ hovered = signal(null, /* @ts-ignore */
2125
+ ...(ngDevMode ? [{ debugName: "hovered" }] : /* istanbul ignore next */ []));
2126
+ /** The day the grid's single tab stop is on. */
2127
+ activeDate = signal(DateTime.now(), /* @ts-ignore */
2128
+ ...(ngDevMode ? [{ debugName: "activeDate" }] : /* istanbul ignore next */ []));
2129
+ /** The first month shown. */
2130
+ viewMonth = signal(DateTime.now().startOf('month'), /* @ts-ignore */
2131
+ ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
2132
+ weekdays = computed(() => {
2133
+ const locale = this.locale();
2134
+ const short = Info.weekdays('short', { locale });
2135
+ const long = Info.weekdays('long', { locale });
2136
+ // Luxon starts on Monday; the grid starts on Sunday, as nxe-date-box's does.
2137
+ return [{ short: short[6], long: long[6] }, ...short.slice(0, 6).map((s, i) => ({ short: s, long: long[i] }))];
2138
+ }, /* @ts-ignore */
2139
+ ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
2140
+ presetResults = computed(() => {
2141
+ const now = this.clock().setLocale(this.locale());
2142
+ return this.presets().map((preset) => {
2143
+ const bounds = preset.range(now);
2144
+ return {
2145
+ label: preset.label ?? (preset.labelKey ? this.nxeMessages.t(preset.labelKey) : ''),
2146
+ start: this.clampDay(this.toDay(bounds.start)),
2147
+ end: this.clampDay(this.toDay(bounds.end)),
2148
+ };
2149
+ });
2150
+ }, /* @ts-ignore */
2151
+ ...(ngDevMode ? [{ debugName: "presetResults" }] : /* istanbul ignore next */ []));
2152
+ presetItems = computed(() => {
2153
+ const { start, end } = this.range();
2154
+ return this.presetResults().map((preset, index) => ({
2155
+ index,
2156
+ label: preset.label,
2157
+ active: !!start &&
2158
+ !!end &&
2159
+ !!preset.start &&
2160
+ !!preset.end &&
2161
+ dayKey(preset.start) === dayKey(start) &&
2162
+ dayKey(preset.end) === dayKey(end),
2163
+ }));
2164
+ }, /* @ts-ignore */
2165
+ ...(ngDevMode ? [{ debugName: "presetItems" }] : /* istanbul ignore next */ []));
2166
+ /** The key of the grid's tab stop: the active day while it is on screen, else the first day shown. */
2167
+ tabKey = computed(() => {
2168
+ const first = this.viewMonth().startOf('month');
2169
+ const last = first.plus({ months: this.monthsShown() - 1 }).endOf('month');
2170
+ const active = this.activeDate();
2171
+ return active >= first && active <= last ? dayKey(active) : dayKey(first);
2172
+ }, /* @ts-ignore */
2173
+ ...(ngDevMode ? [{ debugName: "tabKey" }] : /* istanbul ignore next */ []));
2174
+ months = computed(() => {
2175
+ const locale = this.locale();
2176
+ const today = dayKey(this.clock());
2177
+ const min = this.minDate();
2178
+ const max = this.maxDate();
2179
+ const tabKey = this.tabKey();
2180
+ const draft = this.draftStart();
2181
+ const hover = this.hovered();
2182
+ let low;
2183
+ let high;
2184
+ if (draft) {
2185
+ low = draft;
2186
+ high = hover ?? draft;
2187
+ if (high < low) {
2188
+ [low, high] = [high, low];
2189
+ }
2190
+ }
2191
+ else {
2192
+ const { start, end } = this.range();
2193
+ low = start ?? end;
2194
+ high = end ?? start;
2195
+ }
2196
+ const lowKey = low ? dayKey(low) : null;
2197
+ const highKey = high ? dayKey(high) : null;
2198
+ return Array.from({ length: this.monthsShown() }, (_, offset) => {
2199
+ const month = this.viewMonth().plus({ months: offset }).setLocale(locale);
2200
+ const weeks = buildCalendarWeeks(month)
2201
+ .map((week) => week.map((day) => {
2202
+ if (day.month !== month.month) {
2203
+ return null;
2204
+ }
2205
+ const key = dayKey(day);
2206
+ return {
2207
+ date: day,
2208
+ key,
2209
+ day: day.day,
2210
+ label: day.setLocale(locale).toLocaleString(DateTime.DATE_FULL),
2211
+ disabled: (!!min && day < min) || (!!max && day > max),
2212
+ today: key === today,
2213
+ start: key === lowKey,
2214
+ end: key === highKey,
2215
+ inRange: !!lowKey && !!highKey && key >= lowKey && key <= highKey,
2216
+ preview: !!draft,
2217
+ tabbable: key === tabKey,
2218
+ };
2219
+ }))
2220
+ .filter((week) => week.some((cell) => cell !== null));
2221
+ return { key: dayKey(month.startOf('month')), caption: month.toFormat('LLLL yyyy'), weeks };
2222
+ });
2223
+ }, /* @ts-ignore */
2224
+ ...(ngDevMode ? [{ debugName: "months" }] : /* istanbul ignore next */ []));
2225
+ canGoBack = computed(() => {
2226
+ const min = this.minDate();
2227
+ return !min || this.viewMonth().startOf('month') > min.startOf('month');
2228
+ }, /* @ts-ignore */
2229
+ ...(ngDevMode ? [{ debugName: "canGoBack" }] : /* istanbul ignore next */ []));
2230
+ canGoForward = computed(() => {
2231
+ const max = this.maxDate();
2232
+ return !max || this.viewMonth().plus({ months: this.monthsShown() - 1 }).endOf('month') < max.startOf('month');
2233
+ }, /* @ts-ignore */
2234
+ ...(ngDevMode ? [{ debugName: "canGoForward" }] : /* istanbul ignore next */ []));
2235
+ /** The line under the grid, read out as it changes. */
2236
+ status = computed(() => this.nxeMessages.t(this.draftStart() ? 'dateRange.pickEnd' : 'dateRange.pickStart'), /* @ts-ignore */
2237
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
2238
+ overlayRef;
2239
+ closeSub;
2240
+ flipSub;
2241
+ // --- field ---
2242
+ onFieldClick() {
2243
+ if (this.opened()) {
2244
+ this.close(true);
2245
+ }
2246
+ else {
2247
+ this.open();
2248
+ }
2249
+ }
2250
+ onFieldKeydown(event) {
2251
+ if (event.ctrlKey || event.metaKey) {
2252
+ return;
2253
+ }
2254
+ switch (event.key) {
2255
+ case 'Enter':
2256
+ case ' ':
2257
+ case 'ArrowDown':
2258
+ event.preventDefault();
2259
+ this.open();
2260
+ break;
2261
+ case 'Backspace':
2262
+ case 'Delete':
2263
+ if (!this.readOnly() && !this.disabled()) {
2264
+ event.preventDefault();
2265
+ this.clear(event);
2266
+ }
2267
+ break;
2268
+ }
2269
+ }
2270
+ onFocus() {
2271
+ this.focused.set(true);
2272
+ }
2273
+ onBlur() {
2274
+ this.focused.set(false);
2275
+ // Focus moving into the popup is not leaving the field; the popup marks it touched on close.
2276
+ if (!this.opened()) {
2277
+ this.markAsTouched();
2278
+ }
2279
+ }
2280
+ onClearClick(event) {
2281
+ this.clear(event);
2282
+ this.fieldRef().nativeElement.focus();
2283
+ }
2284
+ /** Empties the range. The value becomes `null`. */
2285
+ clear(event) {
2286
+ this.draftStart.set(null);
2287
+ this.hovered.set(null);
2288
+ this.commitRange(null, null, event);
2289
+ }
2290
+ // --- popup: day grid ---
2291
+ pick(cell, event) {
2292
+ if (cell.disabled) {
2293
+ return;
2294
+ }
2295
+ this.activeDate.set(cell.date);
2296
+ const draft = this.draftStart();
2297
+ if (!draft) {
2298
+ this.draftStart.set(cell.date);
2299
+ this.hovered.set(cell.date);
2300
+ return;
2301
+ }
2302
+ this.draftStart.set(null);
2303
+ this.hovered.set(null);
2304
+ this.commitRange(draft, cell.date, event);
2305
+ this.close(true);
2306
+ }
2307
+ onDayEnter(cell) {
2308
+ if (this.draftStart()) {
2309
+ this.hovered.set(cell.date);
2310
+ }
2311
+ }
2312
+ onDayFocus(cell) {
2313
+ this.activeDate.set(cell.date);
2314
+ if (this.draftStart()) {
2315
+ this.hovered.set(cell.date);
2316
+ }
2317
+ }
2318
+ /** The pointer left the grids: the preview falls back to the keyboard's day. */
2319
+ onGridLeave() {
2320
+ this.hovered.set(this.draftStart() ? this.activeDate() : null);
2321
+ }
2322
+ onDayKeydown(event, cell) {
2323
+ if (event.ctrlKey || event.metaKey || event.altKey) {
2324
+ return;
2325
+ }
2326
+ let target;
2327
+ switch (event.key) {
2328
+ case 'ArrowLeft':
2329
+ target = cell.date.minus({ days: 1 });
2330
+ break;
2331
+ case 'ArrowRight':
2332
+ target = cell.date.plus({ days: 1 });
2333
+ break;
2334
+ case 'ArrowUp':
2335
+ target = cell.date.minus({ days: 7 });
2336
+ break;
2337
+ case 'ArrowDown':
2338
+ target = cell.date.plus({ days: 7 });
2339
+ break;
2340
+ case 'Home':
2341
+ target = cell.date.minus({ days: cell.date.weekday % 7 });
2342
+ break;
2343
+ case 'End':
2344
+ target = cell.date.plus({ days: 6 - (cell.date.weekday % 7) });
2345
+ break;
2346
+ case 'PageUp':
2347
+ target = cell.date.minus(event.shiftKey ? { years: 1 } : { months: 1 });
2348
+ break;
2349
+ case 'PageDown':
2350
+ target = cell.date.plus(event.shiftKey ? { years: 1 } : { months: 1 });
2351
+ break;
2352
+ case 'Enter':
2353
+ case ' ':
2354
+ event.preventDefault();
2355
+ this.pick(cell, event);
2356
+ return;
2357
+ default:
2358
+ return;
2359
+ }
2360
+ event.preventDefault();
2361
+ this.moveTo(target);
2362
+ }
2363
+ moveTo(date) {
2364
+ this.activeDate.set(date);
2365
+ if (this.draftStart()) {
2366
+ this.hovered.set(date);
2367
+ }
2368
+ this.ensureVisible(date);
2369
+ this.focusDay(date);
2370
+ }
2371
+ ensureVisible(date) {
2372
+ const first = this.viewMonth().startOf('month');
2373
+ const last = first.plus({ months: this.monthsShown() - 1 }).endOf('month');
2374
+ if (date < first) {
2375
+ this.viewMonth.set(date.startOf('month'));
2376
+ }
2377
+ else if (date > last) {
2378
+ this.viewMonth.set(date.startOf('month').minus({ months: this.monthsShown() - 1 }));
2379
+ }
2380
+ }
2381
+ focusDay(date) {
2382
+ const selector = `[data-date="${dayKey(date)}"]`;
2383
+ const find = () => this.overlayRef?.overlayElement.querySelector(selector);
2384
+ const existing = find();
2385
+ if (existing) {
2386
+ existing.focus();
2387
+ }
2388
+ else {
2389
+ afterNextRender(() => find()?.focus(), { injector: this.injector });
2390
+ }
2391
+ }
2392
+ navigate(delta) {
2393
+ this.viewMonth.update((month) => month.plus({ months: delta }));
2394
+ }
2395
+ // --- popup: presets and typed input ---
2396
+ applyPreset(item, event) {
2397
+ const preset = this.presetResults()[item.index];
2398
+ if (!preset) {
2399
+ return;
2400
+ }
2401
+ this.draftStart.set(null);
2402
+ this.hovered.set(null);
2403
+ this.commitRange(preset.start, preset.end, event);
2404
+ this.close(true);
2405
+ }
2406
+ /** Commit the text typed into one end. Empty clears that end; unreadable text is put back. */
2407
+ commitTyped(which, event) {
2408
+ const element = event.target;
2409
+ const text = element.value.trim();
2410
+ const { start, end } = this.range();
2411
+ this.draftStart.set(null);
2412
+ this.hovered.set(null);
2413
+ if (!text) {
2414
+ this.commitRange(which === 'start' ? null : start, which === 'end' ? null : end, event);
2415
+ }
2416
+ else {
2417
+ const parsed = parseDisplay(text, this.displayFormat(), null, this.locale());
2418
+ if (parsed) {
2419
+ const day = this.clampDay(parsed.startOf('day').setLocale(this.locale()));
2420
+ this.commitRange(which === 'start' ? day : start, which === 'end' ? day : end, event);
2421
+ this.activeDate.set(day);
2422
+ this.ensureVisible(day);
2423
+ }
2424
+ }
2425
+ // The bound text is unchanged when the entry was rejected or clamped back, so repaint it.
2426
+ element.value = which === 'start' ? this.startText() : this.endText();
2427
+ }
2428
+ onTypedKeydown(which, event) {
2429
+ if (event.key === 'Enter') {
2430
+ event.preventDefault();
2431
+ this.commitTyped(which, event);
2432
+ }
2433
+ }
2434
+ onClearInPopup(event) {
2435
+ this.clear(event);
2436
+ this.close(true);
2437
+ }
2438
+ // --- value ---
2439
+ toDay(bound) {
2440
+ return parseDateValue(bound)?.startOf('day').setLocale(this.locale()) ?? null;
2441
+ }
2442
+ clampDay(day) {
2443
+ if (!day) {
2444
+ return null;
2445
+ }
2446
+ const min = this.minDate();
2447
+ const max = this.maxDate();
2448
+ if (min && day < min) {
2449
+ return min.setLocale(this.locale());
2450
+ }
2451
+ if (max && day > max) {
2452
+ return max.setLocale(this.locale());
2453
+ }
2454
+ return day;
2455
+ }
2456
+ serialize(day, edge) {
2457
+ const stamp = edge === 'start' ? day.startOf('day') : day.endOf('day').set({ millisecond: 0 });
2458
+ return stamp.setLocale('en').toFormat(this.valueFormat());
2459
+ }
2460
+ commitRange(start, end, event) {
2461
+ if (start && end && end < start) {
2462
+ [start, end] = [end, start];
2463
+ }
2464
+ const next = start || end
2465
+ ? {
2466
+ start: start ? this.serialize(start, 'start') : null,
2467
+ end: end ? this.serialize(end, 'end') : null,
2468
+ }
2469
+ : null;
2470
+ const current = this.value();
2471
+ if ((current?.start ?? null) === (next?.start ?? null) && (current?.end ?? null) === (next?.end ?? null)) {
2472
+ return;
2473
+ }
2474
+ this.commitValue(next, event);
2475
+ }
2476
+ // --- popup lifecycle ---
2477
+ /** Opens the popup on the start of the range (or today). No-op while open, disabled or read-only. */
2478
+ open() {
2479
+ if (this.disabled() || this.readOnly() || this.opened()) {
2480
+ return;
2481
+ }
2482
+ const { start, end } = this.range();
2483
+ const anchor = this.clampDay(start ?? end ?? this.clock().startOf('day').setLocale(this.locale()));
2484
+ this.draftStart.set(null);
2485
+ this.hovered.set(null);
2486
+ this.activeDate.set(anchor);
2487
+ this.viewMonth.set(anchor.startOf('month'));
2488
+ if (!this.overlayRef) {
2489
+ this.overlayRef = this.overlay.create({
2490
+ positionStrategy: this.overlay
2491
+ .position()
2492
+ .flexibleConnectedTo(this.elementRef)
2493
+ .withPositions([
2494
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
2495
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
2496
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
2497
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' },
2498
+ ])
2499
+ .withFlexibleDimensions(false)
2500
+ .withPush(true)
2501
+ .withViewportMargin(8),
2502
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
2503
+ hasBackdrop: true,
2504
+ backdropClass: 'cdk-overlay-transparent-backdrop',
2505
+ panelClass: nxePane('nxe-daterange-pane'),
2506
+ });
2507
+ this.flipSub = trackPaneFlip(this.overlayRef);
2508
+ }
2509
+ this.overlayRef.attach(new TemplatePortal(this.popupTemplate(), this.viewContainerRef));
2510
+ this.closeSub = merge(this.overlayRef.backdropClick().pipe(map(() => false)), this.overlayRef.keydownEvents().pipe(filter((event) => event.key === 'Escape'), map((event) => {
2511
+ event.preventDefault();
2512
+ return true;
2513
+ }))).subscribe((restoreFocus) => this.close(restoreFocus));
2514
+ this.opened.set(true);
2515
+ this.focusDay(anchor);
2516
+ }
2517
+ /** Closes the popup, dropping a range that was only half picked. `restoreFocus` returns focus to the field. */
2518
+ close(restoreFocus = true) {
2519
+ if (!this.overlayRef?.hasAttached()) {
2520
+ return;
2521
+ }
2522
+ this.overlayRef.detach();
2523
+ this.closeSub?.unsubscribe();
2524
+ this.draftStart.set(null);
2525
+ this.hovered.set(null);
2526
+ this.opened.set(false);
2527
+ this.markAsTouched();
2528
+ if (restoreFocus) {
2529
+ this.fieldRef().nativeElement.focus();
2530
+ }
2531
+ }
2532
+ ngOnDestroy() {
2533
+ this.closeSub?.unsubscribe();
2534
+ this.flipSub?.unsubscribe();
2535
+ this.overlayRef?.dispose();
2536
+ }
2537
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateRangeBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
2538
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDateRangeBoxComponent, isStandalone: true, selector: "nxe-date-range-box", inputs: { displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, showClearButton: { classPropertyName: "showClearButton", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-daterangebox" }, providers: [
2539
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateRangeBoxComponent), multi: true },
2540
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateRangeBoxComponent) },
2541
+ ], viewQueries: [{ propertyName: "popupTemplate", first: true, predicate: ["popupTemplate"], descendants: true, isSignal: true }, { propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container\">\n <!-- A read-only combobox rather than a button: a native input takes its caption from\n <label for>, and nxe-form-field writes aria-invalid / aria-describedby onto it. -->\n <input\n #field\n class=\"nxe-editor-input nxe-daterange-input\"\n type=\"text\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n autocomplete=\"off\"\n readonly\n [id]=\"controlId()\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-expanded]=\"opened()\"\n [attr.aria-controls]=\"opened() ? popupId : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.placeholder]=\"fieldPlaceholder()\"\n [value]=\"displayValue()\"\n [disabled]=\"disabled()\"\n (click)=\"onFieldClick()\"\n (keydown)=\"onFieldKeydown($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\" />\n @if (clearButtonVisible()) {\n <span class=\"nxe-clear-button-area\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onClearClick($event)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path\n fill-rule=\"evenodd\"\n transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n @if (!readOnly()) {\n <span class=\"nxe-daterange-button\" [class.nxe-daterange-open]=\"opened()\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onFieldClick()\">\n <svg class=\"nxe-daterange-icon\" viewBox=\"0 0 18 18\" focusable=\"false\">\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"3\" />\n <rect x=\"4\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"12\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"5\" y=\"9\" width=\"8\" height=\"2\" />\n </svg>\n </span>\n }\n</div>\n<ng-content />\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #popupTemplate>\n <div\n class=\"nxe-range-popup\"\n role=\"dialog\"\n cdkTrapFocus\n [id]=\"popupId\"\n [class.nxe-range-narrow]=\"isNarrow()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.dialog')\">\n @if (presetItems().length) {\n <div class=\"nxe-range-presets\" role=\"group\" [attr.aria-label]=\"nxeMessages.t('dateRange.presets')\">\n @for (preset of presetItems(); track preset.index) {\n <button\n type=\"button\"\n class=\"nxe-range-preset\"\n [class.nxe-range-preset-active]=\"preset.active\"\n [attr.aria-pressed]=\"preset.active\"\n (click)=\"applyPreset(preset, $event)\">\n {{ preset.label }}\n </button>\n }\n </div>\n }\n <div class=\"nxe-range-main\">\n <div class=\"nxe-range-inputs\">\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.start')\"\n [attr.placeholder]=\"startHint()\"\n [value]=\"startText()\"\n (change)=\"commitTyped('start', $event)\"\n (keydown)=\"onTypedKeydown('start', $event)\" />\n <span class=\"nxe-range-dash\" aria-hidden=\"true\">&ndash;</span>\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.end')\"\n [attr.placeholder]=\"endHint()\"\n [value]=\"endText()\"\n (change)=\"commitTyped('end', $event)\"\n (keydown)=\"onTypedKeydown('end', $event)\" />\n </div>\n <div class=\"nxe-range-months\" (mouseleave)=\"onGridLeave()\">\n @for (month of months(); track month.key; let first = $first; let last = $last) {\n <div class=\"nxe-range-month\">\n <div class=\"nxe-range-header\">\n @if (first) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoBack()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.previousMonth')\"\n (click)=\"navigate(-1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"4.875 9 11.875 2 13.125 3.25 7.375 9 13.125 14.75 11.875 16\" />\n </svg>\n </button>\n }\n <div class=\"nxe-range-caption\" aria-live=\"polite\">{{ month.caption }}</div>\n @if (last) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoForward()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.nextMonth')\"\n (click)=\"navigate(1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"13.125 9 6.125 2 4.875 3.25 10.625 9 4.875 14.75 6.125 16\" />\n </svg>\n </button>\n }\n </div>\n <table class=\"nxe-range-grid\" role=\"grid\" [attr.aria-label]=\"month.caption\">\n <thead>\n <tr>\n @for (weekday of weekdays(); track $index) {\n <th scope=\"col\" [attr.abbr]=\"weekday.long\">{{ weekday.short }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (week of month.weeks; track $index) {\n <tr>\n @for (cell of week; track $index) {\n @if (cell) {\n <td\n role=\"gridcell\"\n class=\"nxe-range-cell\"\n [class.nxe-range-in]=\"cell.inRange\"\n [class.nxe-range-start]=\"cell.start && cell.inRange\"\n [class.nxe-range-end]=\"cell.end && cell.inRange\"\n [class.nxe-range-preview]=\"cell.preview && cell.inRange\"\n [attr.aria-selected]=\"cell.inRange\">\n <button\n type=\"button\"\n class=\"nxe-range-day\"\n [class.nxe-range-today]=\"cell.today\"\n [class.nxe-range-edge]=\"(cell.start || cell.end) && cell.inRange\"\n [attr.data-date]=\"cell.key\"\n [attr.tabindex]=\"cell.tabbable ? 0 : -1\"\n [attr.aria-label]=\"cell.label\"\n [attr.aria-disabled]=\"cell.disabled ? true : null\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"pick(cell, $event)\"\n (mouseenter)=\"onDayEnter(cell)\"\n (focus)=\"onDayFocus(cell)\"\n (keydown)=\"onDayKeydown($event, cell)\"\n (keyup.space)=\"$event.preventDefault()\">\n <span>{{ cell.day }}</span>\n </button>\n </td>\n } @else {\n <td class=\"nxe-range-gap\" role=\"presentation\"></td>\n }\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n <div class=\"nxe-range-footer\">\n <span class=\"nxe-range-status\" role=\"status\">{{ status() }}</span>\n <button type=\"button\" class=\"nxe-range-clear\" (click)=\"onClearInPopup($event)\">{{ nxeMessages.t('common.clear') }}</button>\n </div>\n </div>\n </div>\n</ng-template>\n", styles: ["nxe-date-range-box .nxe-daterange-input{cursor:pointer;text-overflow:ellipsis}nxe-date-range-box .nxe-daterange-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-range-box .nxe-daterange-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-range-box:hover:not(.nxe-disabled) .nxe-daterange-button,nxe-date-range-box:active:not(.nxe-disabled) .nxe-daterange-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-range-box .nxe-daterange-button.nxe-daterange-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-range-box .nxe-daterange-icon{width:18px;height:18px;fill:currentColor}nxe-date-range-box .nxe-editor-container:has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 38px}nxe-date-range-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 72px}nxe-date-range-box.nxe-disabled .nxe-daterange-input,nxe-date-range-box.nxe-readonly .nxe-daterange-input{cursor:default}.cdk-overlay-pane.nxe-daterange-pane{margin:0}.nxe-range-popup{--nxe-range-cell: 36px;box-sizing:border-box;display:flex;max-width:calc(100vw - var(--nxe-ui-space-4));max-height:calc(100vh - var(--nxe-ui-space-4) * 2);overflow:auto;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);-webkit-user-select:none;user-select:none}.nxe-range-popup.nxe-range-narrow{--nxe-range-cell: 38px;flex-direction:column}.nxe-range-presets{display:flex;flex:none;flex-direction:column;gap:2px;box-sizing:border-box;min-width:140px;padding:var(--nxe-ui-space-2);border-inline-end:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface-alt)}.nxe-range-narrow .nxe-range-presets{flex-flow:row wrap;min-width:0;border-inline-end:0;border-bottom:1px solid var(--nxe-ui-border)}.nxe-range-preset{padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;text-align:start;white-space:nowrap;cursor:pointer}.nxe-range-preset:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-preset:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-preset.nxe-range-preset-active{background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-main{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-2)}.nxe-range-inputs{display:flex;align-items:center;gap:var(--nxe-ui-space-2)}.nxe-range-text{flex:1 1 0;min-width:0;box-sizing:border-box;height:2.2em;padding:0 .5em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);font:inherit;-webkit-user-select:text;user-select:text}.nxe-range-text:focus{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-text::placeholder{color:var(--nxe-ui-text-muted)}.nxe-range-months{display:flex;gap:var(--nxe-ui-space-3)}.nxe-range-month{flex:none}.nxe-range-header{display:flex;align-items:center;height:32px}.nxe-range-caption{flex:1;font-weight:600;text-align:center}.nxe-range-nav{display:inline-flex;flex:none;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}.nxe-range-nav svg{width:18px;height:18px;fill:currentColor}.nxe-range-nav:hover:not(:disabled){background:var(--nxe-ui-hover-bg)}.nxe-range-nav:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-nav:disabled{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-month:first-child:not(:last-child) .nxe-range-header:after,.nxe-range-month:last-child:not(:first-child) .nxe-range-header:before{content:\"\";flex:none;width:32px}.nxe-range-grid{border-collapse:collapse;table-layout:fixed;width:calc(var(--nxe-range-cell) * 7)}.nxe-range-grid th{height:24px;padding:0;color:var(--nxe-ui-text-muted);font-size:.85em;font-weight:500;text-align:center}.nxe-range-grid td{height:calc(var(--nxe-range-cell) - 4px);padding:0;text-align:center}.nxe-range-cell.nxe-range-in{background:var(--nxe-ui-accent-tint)}.nxe-range-cell.nxe-range-in.nxe-range-start{border-start-start-radius:16px;border-end-start-radius:16px}.nxe-range-cell.nxe-range-in.nxe-range-end{border-start-end-radius:16px;border-end-end-radius:16px}.nxe-range-day{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;padding:0;border:0;border-radius:16px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-day:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent-tint)}.nxe-range-day:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-day[aria-disabled=true]{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-day.nxe-range-today{box-shadow:inset 0 0 0 2px var(--nxe-ui-accent)}.nxe-range-day.nxe-range-edge,.nxe-range-day.nxe-range-edge:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-footer{display:flex;align-items:center;justify-content:space-between;gap:var(--nxe-ui-space-2);padding-top:var(--nxe-ui-space-2);border-top:1px solid var(--nxe-ui-border)}.nxe-range-status{color:var(--nxe-ui-text-muted)}.nxe-range-clear{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-clear:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-clear:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}\n"], dependencies: [{ kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2542
+ }
2543
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateRangeBoxComponent, decorators: [{
2544
+ type: Component,
2545
+ args: [{ selector: 'nxe-date-range-box', imports: [CdkTrapFocus], host: {
2546
+ class: 'nxe-widget nxe-editor nxe-daterangebox',
2547
+ '[class.nxe-focused]': 'focused()',
2548
+ '[class.nxe-invalid]': 'invalidState()',
2549
+ }, providers: [
2550
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateRangeBoxComponent), multi: true },
2551
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateRangeBoxComponent) },
2552
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container\">\n <!-- A read-only combobox rather than a button: a native input takes its caption from\n <label for>, and nxe-form-field writes aria-invalid / aria-describedby onto it. -->\n <input\n #field\n class=\"nxe-editor-input nxe-daterange-input\"\n type=\"text\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n autocomplete=\"off\"\n readonly\n [id]=\"controlId()\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-expanded]=\"opened()\"\n [attr.aria-controls]=\"opened() ? popupId : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.placeholder]=\"fieldPlaceholder()\"\n [value]=\"displayValue()\"\n [disabled]=\"disabled()\"\n (click)=\"onFieldClick()\"\n (keydown)=\"onFieldKeydown($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\" />\n @if (clearButtonVisible()) {\n <span class=\"nxe-clear-button-area\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onClearClick($event)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path\n fill-rule=\"evenodd\"\n transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n @if (!readOnly()) {\n <span class=\"nxe-daterange-button\" [class.nxe-daterange-open]=\"opened()\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onFieldClick()\">\n <svg class=\"nxe-daterange-icon\" viewBox=\"0 0 18 18\" focusable=\"false\">\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"3\" />\n <rect x=\"4\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"12\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"5\" y=\"9\" width=\"8\" height=\"2\" />\n </svg>\n </span>\n }\n</div>\n<ng-content />\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #popupTemplate>\n <div\n class=\"nxe-range-popup\"\n role=\"dialog\"\n cdkTrapFocus\n [id]=\"popupId\"\n [class.nxe-range-narrow]=\"isNarrow()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.dialog')\">\n @if (presetItems().length) {\n <div class=\"nxe-range-presets\" role=\"group\" [attr.aria-label]=\"nxeMessages.t('dateRange.presets')\">\n @for (preset of presetItems(); track preset.index) {\n <button\n type=\"button\"\n class=\"nxe-range-preset\"\n [class.nxe-range-preset-active]=\"preset.active\"\n [attr.aria-pressed]=\"preset.active\"\n (click)=\"applyPreset(preset, $event)\">\n {{ preset.label }}\n </button>\n }\n </div>\n }\n <div class=\"nxe-range-main\">\n <div class=\"nxe-range-inputs\">\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.start')\"\n [attr.placeholder]=\"startHint()\"\n [value]=\"startText()\"\n (change)=\"commitTyped('start', $event)\"\n (keydown)=\"onTypedKeydown('start', $event)\" />\n <span class=\"nxe-range-dash\" aria-hidden=\"true\">&ndash;</span>\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.end')\"\n [attr.placeholder]=\"endHint()\"\n [value]=\"endText()\"\n (change)=\"commitTyped('end', $event)\"\n (keydown)=\"onTypedKeydown('end', $event)\" />\n </div>\n <div class=\"nxe-range-months\" (mouseleave)=\"onGridLeave()\">\n @for (month of months(); track month.key; let first = $first; let last = $last) {\n <div class=\"nxe-range-month\">\n <div class=\"nxe-range-header\">\n @if (first) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoBack()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.previousMonth')\"\n (click)=\"navigate(-1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"4.875 9 11.875 2 13.125 3.25 7.375 9 13.125 14.75 11.875 16\" />\n </svg>\n </button>\n }\n <div class=\"nxe-range-caption\" aria-live=\"polite\">{{ month.caption }}</div>\n @if (last) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoForward()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.nextMonth')\"\n (click)=\"navigate(1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"13.125 9 6.125 2 4.875 3.25 10.625 9 4.875 14.75 6.125 16\" />\n </svg>\n </button>\n }\n </div>\n <table class=\"nxe-range-grid\" role=\"grid\" [attr.aria-label]=\"month.caption\">\n <thead>\n <tr>\n @for (weekday of weekdays(); track $index) {\n <th scope=\"col\" [attr.abbr]=\"weekday.long\">{{ weekday.short }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (week of month.weeks; track $index) {\n <tr>\n @for (cell of week; track $index) {\n @if (cell) {\n <td\n role=\"gridcell\"\n class=\"nxe-range-cell\"\n [class.nxe-range-in]=\"cell.inRange\"\n [class.nxe-range-start]=\"cell.start && cell.inRange\"\n [class.nxe-range-end]=\"cell.end && cell.inRange\"\n [class.nxe-range-preview]=\"cell.preview && cell.inRange\"\n [attr.aria-selected]=\"cell.inRange\">\n <button\n type=\"button\"\n class=\"nxe-range-day\"\n [class.nxe-range-today]=\"cell.today\"\n [class.nxe-range-edge]=\"(cell.start || cell.end) && cell.inRange\"\n [attr.data-date]=\"cell.key\"\n [attr.tabindex]=\"cell.tabbable ? 0 : -1\"\n [attr.aria-label]=\"cell.label\"\n [attr.aria-disabled]=\"cell.disabled ? true : null\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"pick(cell, $event)\"\n (mouseenter)=\"onDayEnter(cell)\"\n (focus)=\"onDayFocus(cell)\"\n (keydown)=\"onDayKeydown($event, cell)\"\n (keyup.space)=\"$event.preventDefault()\">\n <span>{{ cell.day }}</span>\n </button>\n </td>\n } @else {\n <td class=\"nxe-range-gap\" role=\"presentation\"></td>\n }\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n <div class=\"nxe-range-footer\">\n <span class=\"nxe-range-status\" role=\"status\">{{ status() }}</span>\n <button type=\"button\" class=\"nxe-range-clear\" (click)=\"onClearInPopup($event)\">{{ nxeMessages.t('common.clear') }}</button>\n </div>\n </div>\n </div>\n</ng-template>\n", styles: ["nxe-date-range-box .nxe-daterange-input{cursor:pointer;text-overflow:ellipsis}nxe-date-range-box .nxe-daterange-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-range-box .nxe-daterange-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-range-box:hover:not(.nxe-disabled) .nxe-daterange-button,nxe-date-range-box:active:not(.nxe-disabled) .nxe-daterange-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-range-box .nxe-daterange-button.nxe-daterange-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-range-box .nxe-daterange-icon{width:18px;height:18px;fill:currentColor}nxe-date-range-box .nxe-editor-container:has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 38px}nxe-date-range-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 72px}nxe-date-range-box.nxe-disabled .nxe-daterange-input,nxe-date-range-box.nxe-readonly .nxe-daterange-input{cursor:default}.cdk-overlay-pane.nxe-daterange-pane{margin:0}.nxe-range-popup{--nxe-range-cell: 36px;box-sizing:border-box;display:flex;max-width:calc(100vw - var(--nxe-ui-space-4));max-height:calc(100vh - var(--nxe-ui-space-4) * 2);overflow:auto;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);-webkit-user-select:none;user-select:none}.nxe-range-popup.nxe-range-narrow{--nxe-range-cell: 38px;flex-direction:column}.nxe-range-presets{display:flex;flex:none;flex-direction:column;gap:2px;box-sizing:border-box;min-width:140px;padding:var(--nxe-ui-space-2);border-inline-end:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface-alt)}.nxe-range-narrow .nxe-range-presets{flex-flow:row wrap;min-width:0;border-inline-end:0;border-bottom:1px solid var(--nxe-ui-border)}.nxe-range-preset{padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;text-align:start;white-space:nowrap;cursor:pointer}.nxe-range-preset:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-preset:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-preset.nxe-range-preset-active{background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-main{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-2)}.nxe-range-inputs{display:flex;align-items:center;gap:var(--nxe-ui-space-2)}.nxe-range-text{flex:1 1 0;min-width:0;box-sizing:border-box;height:2.2em;padding:0 .5em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);font:inherit;-webkit-user-select:text;user-select:text}.nxe-range-text:focus{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-text::placeholder{color:var(--nxe-ui-text-muted)}.nxe-range-months{display:flex;gap:var(--nxe-ui-space-3)}.nxe-range-month{flex:none}.nxe-range-header{display:flex;align-items:center;height:32px}.nxe-range-caption{flex:1;font-weight:600;text-align:center}.nxe-range-nav{display:inline-flex;flex:none;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}.nxe-range-nav svg{width:18px;height:18px;fill:currentColor}.nxe-range-nav:hover:not(:disabled){background:var(--nxe-ui-hover-bg)}.nxe-range-nav:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-nav:disabled{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-month:first-child:not(:last-child) .nxe-range-header:after,.nxe-range-month:last-child:not(:first-child) .nxe-range-header:before{content:\"\";flex:none;width:32px}.nxe-range-grid{border-collapse:collapse;table-layout:fixed;width:calc(var(--nxe-range-cell) * 7)}.nxe-range-grid th{height:24px;padding:0;color:var(--nxe-ui-text-muted);font-size:.85em;font-weight:500;text-align:center}.nxe-range-grid td{height:calc(var(--nxe-range-cell) - 4px);padding:0;text-align:center}.nxe-range-cell.nxe-range-in{background:var(--nxe-ui-accent-tint)}.nxe-range-cell.nxe-range-in.nxe-range-start{border-start-start-radius:16px;border-end-start-radius:16px}.nxe-range-cell.nxe-range-in.nxe-range-end{border-start-end-radius:16px;border-end-end-radius:16px}.nxe-range-day{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;padding:0;border:0;border-radius:16px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-day:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent-tint)}.nxe-range-day:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-day[aria-disabled=true]{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-day.nxe-range-today{box-shadow:inset 0 0 0 2px var(--nxe-ui-accent)}.nxe-range-day.nxe-range-edge,.nxe-range-day.nxe-range-edge:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-footer{display:flex;align-items:center;justify-content:space-between;gap:var(--nxe-ui-space-2);padding-top:var(--nxe-ui-space-2);border-top:1px solid var(--nxe-ui-border)}.nxe-range-status{color:var(--nxe-ui-text-muted)}.nxe-range-clear{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-clear:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-clear:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}\n"] }]
2553
+ }], propDecorators: { popupTemplate: [{ type: i0.ViewChild, args: ['popupTemplate', { isSignal: true }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], showClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }] } });
1078
2554
 
1079
2555
  /**
1080
2556
  * `nxe-scroll-view` — drop-in replacement for DevExtreme `dx-scroll-view`.
1081
2557
  *
1082
- * In this app `dx-scroll-view` is used purely as a dumb scroll container: no `@ViewChild`, no
2558
+ * In this app `nxe-scroll-view` is used purely as a dumb scroll container: no `@ViewChild`, no
1083
2559
  * `.instance` methods, no events (see survey). Every live tag is the same literal triple
1084
2560
  * `[scrollByContent]="true" [scrollByThumb]="true" [showScrollbar]="'onScroll'"`. So this is a
1085
2561
  * styled `overflow:auto` wrapper that projects its content inside a `.nxe-scrollview-content`
@@ -1096,18 +2572,792 @@ class NxeScrollViewComponent {
1096
2572
  showScrollbar = input('onScroll', /* @ts-ignore */
1097
2573
  ...(ngDevMode ? [{ debugName: "showScrollbar" }] : /* istanbul ignore next */ []));
1098
2574
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1099
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeScrollViewComponent, isStandalone: true, selector: "nxe-scroll-view", inputs: { scrollByContent: { classPropertyName: "scrollByContent", publicName: "scrollByContent", isSignal: true, isRequired: false, transformFunction: null }, scrollByThumb: { classPropertyName: "scrollByThumb", publicName: "scrollByThumb", isSignal: true, isRequired: false, transformFunction: null }, showScrollbar: { classPropertyName: "showScrollbar", publicName: "showScrollbar", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-widget nxe-scroll-view" }, ngImport: i0, template: '<div class="nxe-scrollview-content"><ng-content /></div>', isInline: true, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(191,191,191,.7) transparent}nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:#bfbfbfb3;border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:#bfbfbf}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2575
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeScrollViewComponent, isStandalone: true, selector: "nxe-scroll-view", inputs: { scrollByContent: { classPropertyName: "scrollByContent", publicName: "scrollByContent", isSignal: true, isRequired: false, transformFunction: null }, scrollByThumb: { classPropertyName: "scrollByThumb", publicName: "scrollByThumb", isSignal: true, isRequired: false, transformFunction: null }, showScrollbar: { classPropertyName: "showScrollbar", publicName: "showScrollbar", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-widget nxe-scroll-view" }, ngImport: i0, template: '<div class="nxe-scrollview-content"><ng-content /></div>', isInline: true, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7)) transparent}@supports not (scrollbar-width: thin){nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7));border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb-hover, rgb(191, 191, 191))}}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1100
2576
  }
1101
2577
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollViewComponent, decorators: [{
1102
2578
  type: Component,
1103
- args: [{ selector: 'nxe-scroll-view', standalone: true, template: '<div class="nxe-scrollview-content"><ng-content /></div>', host: { class: 'nxe-widget nxe-scroll-view' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(191,191,191,.7) transparent}nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:#bfbfbfb3;border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:#bfbfbf}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"] }]
2579
+ args: [{ selector: 'nxe-scroll-view', standalone: true, template: '<div class="nxe-scrollview-content"><ng-content /></div>', host: { class: 'nxe-widget nxe-scroll-view' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7)) transparent}@supports not (scrollbar-width: thin){nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7));border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb-hover, rgb(191, 191, 191))}}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"] }]
1104
2580
  }], propDecorators: { scrollByContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollByContent", required: false }] }], scrollByThumb: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollByThumb", required: false }] }], showScrollbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showScrollbar", required: false }] }] } });
1105
2581
 
2582
+ /** Key used when a failing validator has no entry and carries no message of its own. */
2583
+ const NXE_FALLBACK_ERROR_KEY = 'invalid';
2584
+ /** English defaults for Angular's built-in validators. */
2585
+ const NXE_DEFAULT_ERROR_MESSAGES = {
2586
+ required: 'This field is required.',
2587
+ email: 'Enter a valid email address.',
2588
+ minlength: (e) => `Enter at least ${e?.requiredLength} characters.`,
2589
+ maxlength: (e) => `Enter no more than ${e?.requiredLength} characters.`,
2590
+ min: (e) => `Must be at least ${e?.min}.`,
2591
+ max: (e) => `Must be at most ${e?.max}.`,
2592
+ pattern: 'Enter a value in the expected format.',
2593
+ [NXE_FALLBACK_ERROR_KEY]: 'This value is not valid.',
2594
+ };
2595
+ /**
2596
+ * Validator-key to message map read by `nxe-form-field`.
2597
+ *
2598
+ * Provide a plain object to replace it; keys you leave out fall back to
2599
+ * {@link NXE_DEFAULT_ERROR_MESSAGES}, so a partial map is fine. {@link provideNxeErrorMessages} is
2600
+ * the short way to write the provider. A custom validator is covered by adding its key:
2601
+ * `{ uen: 'Enter a valid UEN.' }`.
2602
+ */
2603
+ const NXE_ERROR_MESSAGES = new InjectionToken('NXE_ERROR_MESSAGES', {
2604
+ providedIn: 'root',
2605
+ factory: () => NXE_DEFAULT_ERROR_MESSAGES,
2606
+ });
2607
+ /**
2608
+ * Registers messages for `nxe-form-field`, layered over the English defaults:
2609
+ *
2610
+ * ```ts
2611
+ * provideNxeErrorMessages({
2612
+ * required: 'Wajib diisi.',
2613
+ * minlength: (e) => `Minimal ${e.requiredLength} karakter.`,
2614
+ * })
2615
+ * ```
2616
+ */
2617
+ function provideNxeErrorMessages(messages) {
2618
+ return makeEnvironmentProviders([
2619
+ { provide: NXE_ERROR_MESSAGES, useValue: { ...NXE_DEFAULT_ERROR_MESSAGES, ...messages } },
2620
+ ]);
2621
+ }
2622
+
2623
+ let nextFieldId = 0;
2624
+ function toList(value) {
2625
+ if (value == null) {
2626
+ return [];
2627
+ }
2628
+ return (typeof value === 'string' ? [value] : [...value]).filter((m) => !!m);
2629
+ }
2630
+ /** Signal Forms error kinds to the validator keys the message tables use. */
2631
+ const SIGNAL_KIND_TO_KEY = {
2632
+ minLength: 'minlength',
2633
+ maxLength: 'maxlength',
2634
+ };
2635
+ /**
2636
+ * `nxe-form-field` wraps one `nxe-*` editor that is bound to a form control (`formControlName`,
2637
+ * `[formControl]` or `ngModel`) and gives it the chrome a form needs:
2638
+ *
2639
+ * - a `label`, associated with the editor's inner control (`for` = the editor's `inputId`);
2640
+ * - a required marker, from `Validators.required` on the control or the `required` input;
2641
+ * - a `hint`, replaced by the error while one shows;
2642
+ * - the message for the FIRST failing validator once the control is touched or its form submitted
2643
+ * (`submitted` forces it), looked up in {@link NXE_ERROR_MESSAGES};
2644
+ * - `serverErrors` / `serverError` from an API response, shown regardless of touched.
2645
+ *
2646
+ * It sets `aria-invalid`, `aria-required` and `aria-describedby` on the editor's focusable element
2647
+ * and the editor host's `nxe-invalid` class, so the border, badge and colour come from the same
2648
+ * `--nxe-ui-invalid*` tokens as an editor invalidated by `nxe-validator`.
2649
+ *
2650
+ * Reactivity is the control's own `events` stream (value, status, touched, submit, reset) feeding
2651
+ * signals; the DOM sync runs through `afterRenderEffect`, which names the one computed it reads.
2652
+ */
2653
+ class NxeFormFieldComponent {
2654
+ destroyRef = inject(DestroyRef);
2655
+ tokenMessages = inject(NXE_ERROR_MESSAGES);
2656
+ messageService = inject(NxeMessageService);
2657
+ /** The enclosing form, when there is one: a template-driven control joins it a tick late. */
2658
+ container = inject(ControlContainer, { optional: true });
2659
+ /** Caption above the editor. */
2660
+ label = input(undefined, /* @ts-ignore */
2661
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2662
+ /** Help text under the editor; hidden while an error is showing. */
2663
+ hint = input(undefined, /* @ts-ignore */
2664
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
2665
+ /**
2666
+ * Force the required marker on or off. Unset, it follows `Validators.required` (and
2667
+ * `requiredTrue`) on the control. Needed for a rule the control does not declare as a validator.
2668
+ */
2669
+ required = input(undefined, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
2670
+ /**
2671
+ * API field errors, shown whether or not the control was touched. A string or a list of them;
2672
+ * the app clears them (set `null`) when the user edits the field or resubmits.
2673
+ */
2674
+ serverErrors = input(undefined, /* @ts-ignore */
2675
+ ...(ngDevMode ? [{ debugName: "serverErrors" }] : /* istanbul ignore next */ []));
2676
+ /** Singular spelling of {@link serverErrors}; both may be set, their messages are joined. */
2677
+ serverError = input(undefined, /* @ts-ignore */
2678
+ ...(ngDevMode ? [{ debugName: "serverError" }] : /* istanbul ignore next */ []));
2679
+ /**
2680
+ * Show client-side errors now (`true`) or never (`false`). Unset, they show once the control is
2681
+ * touched or the surrounding form was submitted.
2682
+ */
2683
+ submitted = input(undefined, { ...(ngDevMode ? { debugName: "submitted" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
2684
+ /** Messages for this field only, by validator key; override {@link NXE_ERROR_MESSAGES}. */
2685
+ errorMessages = input(undefined, /* @ts-ignore */
2686
+ ...(ngDevMode ? [{ debugName: "errorMessages" }] : /* istanbul ignore next */ []));
2687
+ ngControl = contentChild(NgControl, /* @ts-ignore */
2688
+ ...(ngDevMode ? [{ debugName: "ngControl" }] : /* istanbul ignore next */ []));
2689
+ ngControlHost = contentChild(NgControl, { ...(ngDevMode ? { debugName: "ngControlHost" } : /* istanbul ignore next */ {}), read: ElementRef });
2690
+ /**
2691
+ * Set when the projected editor carries Angular's Signal Forms `[formField]`. That directive
2692
+ * provides a stand-in `NgControl` with no `events` stream, so the field then reads the field
2693
+ * state's signals instead of subscribing to a control.
2694
+ */
2695
+ formField = contentChild(FormField, /* @ts-ignore */
2696
+ ...(ngDevMode ? [{ debugName: "formField" }] : /* istanbul ignore next */ []));
2697
+ formFieldHost = contentChild(FormField, { ...(ngDevMode ? { debugName: "formFieldHost" } : /* istanbul ignore next */ {}), read: ElementRef });
2698
+ editorHost = computed(() => this.formFieldHost() ?? this.ngControlHost(), /* @ts-ignore */
2699
+ ...(ngDevMode ? [{ debugName: "editorHost" }] : /* istanbul ignore next */ []));
2700
+ fieldState = computed(() => this.formField()?.state() ?? null, /* @ts-ignore */
2701
+ ...(ngDevMode ? [{ debugName: "fieldState" }] : /* istanbul ignore next */ []));
2702
+ /** Only read in dev mode, to catch a second editor the field would silently not bind. */
2703
+ allControls = contentChildren(NgControl, { ...(ngDevMode ? { debugName: "allControls" } : /* istanbul ignore next */ {}), descendants: true });
2704
+ // Mirrors of the control's state. The control is not a signal, so each event on it copies the
2705
+ // parts the template reads into these.
2706
+ touched = signal(false, /* @ts-ignore */
2707
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2708
+ controlErrors = signal(null, /* @ts-ignore */
2709
+ ...(ngDevMode ? [{ debugName: "controlErrors" }] : /* istanbul ignore next */ []));
2710
+ validatorRequired = signal(false, /* @ts-ignore */
2711
+ ...(ngDevMode ? [{ debugName: "validatorRequired" }] : /* istanbul ignore next */ []));
2712
+ formSubmitted = signal(false, /* @ts-ignore */
2713
+ ...(ngDevMode ? [{ debugName: "formSubmitted" }] : /* istanbul ignore next */ []));
2714
+ editor = signal(null, /* @ts-ignore */
2715
+ ...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
2716
+ uid = `nxe-form-field-${++nextFieldId}`;
2717
+ labelId = `${this.uid}-label`;
2718
+ hintId = `${this.uid}-hint`;
2719
+ errorId = `${this.uid}-error`;
2720
+ /** The id the label points at: the editor's inner control. Null for a non-nxe editor. */
2721
+ controlId = computed(() => this.editor()?.controlId() ?? null, /* @ts-ignore */
2722
+ ...(ngDevMode ? [{ debugName: "controlId" }] : /* istanbul ignore next */ []));
2723
+ isRequired = computed(() => this.required() ?? (this.fieldState()?.required() ?? this.validatorRequired()), /* @ts-ignore */
2724
+ ...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
2725
+ reveal = computed(() => {
2726
+ const state = this.fieldState();
2727
+ // A Signal Forms submit marks every field touched, so touched covers a submitted form.
2728
+ return this.submitted() ?? (state ? state.touched() : this.touched() || this.formSubmitted());
2729
+ }, /* @ts-ignore */
2730
+ ...(ngDevMode ? [{ debugName: "reveal" }] : /* istanbul ignore next */ []));
2731
+ clientMessage = computed(() => {
2732
+ if (!this.reveal()) {
2733
+ return null;
2734
+ }
2735
+ const state = this.fieldState();
2736
+ if (state) {
2737
+ const first = state.errors()[0];
2738
+ return first ? this.resolveSignalError(first) : null;
2739
+ }
2740
+ const errors = this.controlErrors();
2741
+ const key = errors ? Object.keys(errors)[0] : undefined;
2742
+ return key === undefined ? null : this.resolve(key, errors[key]);
2743
+ }, /* @ts-ignore */
2744
+ ...(ngDevMode ? [{ debugName: "clientMessage" }] : /* istanbul ignore next */ []));
2745
+ /** Everything shown under the editor as an error: the client message, then the server's. */
2746
+ messages = computed(() => {
2747
+ const client = this.clientMessage();
2748
+ return [
2749
+ ...(client ? [client] : []),
2750
+ ...toList(this.serverErrors()),
2751
+ ...toList(this.serverError()),
2752
+ ];
2753
+ }, /* @ts-ignore */
2754
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
2755
+ invalid = computed(() => this.messages().length > 0, /* @ts-ignore */
2756
+ ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
2757
+ showHint = computed(() => !!this.hint() && !this.invalid(), /* @ts-ignore */
2758
+ ...(ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []));
2759
+ domState = computed(() => ({
2760
+ host: this.editorHost()?.nativeElement ?? null,
2761
+ controlId: this.controlId(),
2762
+ invalid: this.invalid(),
2763
+ required: this.isRequired(),
2764
+ describedBy: [this.showHint() ? this.hintId : null, this.invalid() ? this.errorId : null]
2765
+ .filter((id) => !!id)
2766
+ .join(' ') || null,
2767
+ labelledBy: this.label() ? this.labelId : null,
2768
+ }), /* @ts-ignore */
2769
+ ...(ngDevMode ? [{ debugName: "domState" }] : /* istanbul ignore next */ []));
2770
+ /** Attributes this field put on the editor's element, so it only ever removes its own. */
2771
+ applied = null;
2772
+ invalidClassHost = null;
2773
+ constructor() {
2774
+ // Reads exactly `domState()`; the DOM writes below read no signal.
2775
+ afterRenderEffect(() => this.syncDom(this.domState()));
2776
+ }
2777
+ ngAfterContentInit() {
2778
+ if (typeof ngDevMode !== 'undefined' && ngDevMode && this.allControls().length > 1) {
2779
+ console.warn(`nxe-form-field binds only its first form control, but ${this.allControls().length} are projected. ` +
2780
+ 'Wrap each editor in its own nxe-form-field.');
2781
+ }
2782
+ const formField = this.formField();
2783
+ if (formField) {
2784
+ // Signal Forms: the state is read through signals (fieldState); there is nothing to subscribe to.
2785
+ const accessors = formField.injector.get(NG_VALUE_ACCESSOR, null);
2786
+ this.editor.set((accessors ?? []).find((a) => a instanceof NxeFormControlBase) ?? null);
2787
+ return;
2788
+ }
2789
+ const ngControl = this.ngControl();
2790
+ const control = ngControl?.control ?? null;
2791
+ const accessor = ngControl?.valueAccessor;
2792
+ if (accessor instanceof NxeFormControlBase) {
2793
+ this.editor.set(accessor);
2794
+ }
2795
+ if (!control) {
2796
+ return;
2797
+ }
2798
+ this.syncFromControl(control);
2799
+ control.events
2800
+ .pipe(takeUntilDestroyed(this.destroyRef))
2801
+ .subscribe(() => this.syncFromControl(control));
2802
+ // Submit and reset are emitted on the ROOT of the form, not on each control.
2803
+ (this.container?.control ?? control).root.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
2804
+ if (event instanceof FormSubmittedEvent) {
2805
+ this.formSubmitted.set(true);
2806
+ }
2807
+ else if (event instanceof FormResetEvent) {
2808
+ this.formSubmitted.set(false);
2809
+ }
2810
+ this.syncFromControl(control);
2811
+ });
2812
+ }
2813
+ syncFromControl(control) {
2814
+ this.touched.set(control.touched);
2815
+ this.controlErrors.set(control.errors);
2816
+ this.validatorRequired.set(control.hasValidator(Validators.required) || control.hasValidator(Validators.requiredTrue));
2817
+ }
2818
+ /**
2819
+ * Message for one failing validator: this field's `errorMessages`, then an entry the app put in
2820
+ * the {@link NXE_ERROR_MESSAGES} token, then the active `NxeMessageService` bundle (English
2821
+ * unless the app switched language), then a string the validator itself returned
2822
+ * (`{ uen: 'Not a valid UEN' }` or `{ uen: { message } }`), then the generic fallback.
2823
+ *
2824
+ * A token entry that IS the built-in default (the same reference, which is what
2825
+ * `provideNxeErrorMessages` leaves for the keys it was not given) counts as unset, so it does
2826
+ * not pin English over the bundle. Called from the `clientMessage` computed, so a language
2827
+ * switch re-renders the error.
2828
+ */
2829
+ resolve(key, error) {
2830
+ const entry = this.errorMessages()?.[key] ?? this.explicitToken(key) ?? this.bundled(key, error);
2831
+ if (entry !== undefined) {
2832
+ return typeof entry === 'function' ? entry(error) : entry;
2833
+ }
2834
+ if (typeof error === 'string' && error) {
2835
+ return error;
2836
+ }
2837
+ const own = error?.message;
2838
+ if (typeof own === 'string' && own) {
2839
+ return own;
2840
+ }
2841
+ const fallback = this.errorMessages()?.[NXE_FALLBACK_ERROR_KEY] ??
2842
+ this.explicitToken(NXE_FALLBACK_ERROR_KEY) ??
2843
+ this.messageService.t('validation.field.invalid');
2844
+ return typeof fallback === 'function' ? fallback(error) : fallback;
2845
+ }
2846
+ /**
2847
+ * Message for a Signal Forms error `{ kind, message? }`. The kind is mapped to the validator keys
2848
+ * above (`minLength` to `minlength`, `maxLength` to `maxlength`; `required`, `min`, `max`, `pattern`
2849
+ * and `email` as they are). Lookup order: this field's `errorMessages`, an app token entry, the
2850
+ * error's own `message`, the library bundle, then the fallback (`invalid`). So a rule written
2851
+ * with a message (`required(p.name, { message: '...' })`) shows it, and a custom kind does too.
2852
+ */
2853
+ resolveSignalError(error) {
2854
+ const key = SIGNAL_KIND_TO_KEY[error.kind] ?? error.kind;
2855
+ const e = error;
2856
+ // A validator written for Reactive Forms (nxe-file-input's, or an app's) reaches the field as
2857
+ // `{ kind, context }`, where `context` is what it returned under its key.
2858
+ const context = e['context'];
2859
+ const fromValidator = typeof context === 'object' && context !== null ? context : {};
2860
+ // Shape the payload like the Reactive Forms error of the same key, which the tables read.
2861
+ const payload = { ...fromValidator, ...e, requiredLength: e['minLength'] ?? e['maxLength'] ?? fromValidator['requiredLength'] };
2862
+ const ownMessage = error.message || (typeof context === 'string' ? context : undefined) || (typeof fromValidator['message'] === 'string' ? fromValidator['message'] : undefined);
2863
+ const entry = this.errorMessages()?.[key] ?? this.explicitToken(key);
2864
+ if (entry !== undefined) {
2865
+ return typeof entry === 'function' ? entry(payload) : entry;
2866
+ }
2867
+ if (ownMessage) {
2868
+ return ownMessage;
2869
+ }
2870
+ const bundled = this.bundled(key, payload);
2871
+ if (bundled !== undefined) {
2872
+ return bundled;
2873
+ }
2874
+ const fallback = this.errorMessages()?.[NXE_FALLBACK_ERROR_KEY] ??
2875
+ this.explicitToken(NXE_FALLBACK_ERROR_KEY) ??
2876
+ this.messageService.t('validation.field.invalid');
2877
+ return typeof fallback === 'function' ? fallback(payload) : fallback;
2878
+ }
2879
+ explicitToken(key) {
2880
+ const entry = this.tokenMessages[key];
2881
+ return entry === NXE_DEFAULT_ERROR_MESSAGES[key] ? undefined : entry;
2882
+ }
2883
+ /** The library bundle's wording for Angular's built-in validators. */
2884
+ bundled(key, error) {
2885
+ const e = error;
2886
+ switch (key) {
2887
+ case 'required':
2888
+ case 'email':
2889
+ case 'pattern':
2890
+ return this.messageService.t(`validation.field.${key}`);
2891
+ case 'minlength':
2892
+ return this.messageService.t('validation.field.minlength', e?.requiredLength);
2893
+ case 'maxlength':
2894
+ return this.messageService.t('validation.field.maxlength', e?.requiredLength);
2895
+ case 'min':
2896
+ return this.messageService.t('validation.field.min', e?.min);
2897
+ case 'max':
2898
+ return this.messageService.t('validation.field.max', e?.max);
2899
+ // nxe-file-input's errors: the file type allow-list, the size and count limits, and uploads.
2900
+ case 'fileType':
2901
+ return this.messageService.t('validation.field.fileType', error?.accept);
2902
+ case 'fileSize':
2903
+ return this.messageService.t('validation.field.fileSize', nxeFormatFileSize(Number(error?.maxSize), this.messageService));
2904
+ case 'maxFiles':
2905
+ return this.messageService.t('validation.field.maxFiles', error?.maxFiles);
2906
+ case 'uploading':
2907
+ case 'uploadFailed':
2908
+ return this.messageService.t(`validation.field.${key}`);
2909
+ default:
2910
+ return undefined;
2911
+ }
2912
+ }
2913
+ /** The element a screen reader should treat as the field: the inner control, not the host. */
2914
+ focusable(host, controlId) {
2915
+ if (controlId) {
2916
+ const byId = host.querySelector(`#${CSS.escape(controlId)}`);
2917
+ if (byId) {
2918
+ return byId;
2919
+ }
2920
+ }
2921
+ if (host.matches('[tabindex], input, textarea, select')) {
2922
+ return host;
2923
+ }
2924
+ return (host.querySelector('input, textarea, select, [tabindex]:not([tabindex="-1"])') ??
2925
+ host);
2926
+ }
2927
+ setAttr(name, value) {
2928
+ const { el, attrs } = this.applied;
2929
+ if (value === null) {
2930
+ if (attrs.delete(name)) {
2931
+ el.removeAttribute(name);
2932
+ }
2933
+ }
2934
+ else {
2935
+ el.setAttribute(name, value);
2936
+ attrs.add(name);
2937
+ }
2938
+ }
2939
+ syncDom(state) {
2940
+ if (!state.host) {
2941
+ return;
2942
+ }
2943
+ const el = this.focusable(state.host, state.controlId);
2944
+ if (this.applied?.el !== el) {
2945
+ this.applied = { el, attrs: new Set() };
2946
+ }
2947
+ this.setAttr('aria-invalid', state.invalid ? 'true' : null);
2948
+ this.setAttr('aria-required', state.required ? 'true' : null);
2949
+ this.setAttr('aria-describedby', state.describedBy);
2950
+ // A native input is labelled by the `for` association; the host of an editor without one
2951
+ // (check-box, drop-down) needs the label named explicitly, unless the app already did.
2952
+ const own = this.applied.attrs.has('aria-labelledby');
2953
+ const needsLabelledBy = !!state.labelledBy &&
2954
+ !/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) &&
2955
+ (own || (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')));
2956
+ this.setAttr('aria-labelledby', needsLabelledBy ? state.labelledBy : null);
2957
+ // The editor's own invalid look (border, badge) is keyed off `.nxe-invalid` on its host.
2958
+ if (state.invalid) {
2959
+ state.host.classList.add('nxe-invalid');
2960
+ this.invalidClassHost = state.host;
2961
+ }
2962
+ else if (this.invalidClassHost) {
2963
+ this.invalidClassHost.classList.remove('nxe-invalid');
2964
+ this.invalidClassHost = null;
2965
+ }
2966
+ }
2967
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2968
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeFormFieldComponent, isStandalone: true, selector: "nxe-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, serverErrors: { classPropertyName: "serverErrors", publicName: "serverErrors", isSignal: true, isRequired: false, transformFunction: null }, serverError: { classPropertyName: "serverError", publicName: "serverError", isSignal: true, isRequired: false, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-form-field-invalid": "invalid()" }, classAttribute: "nxe-form-field" }, queries: [{ propertyName: "ngControl", first: true, predicate: NgControl, descendants: true, isSignal: true }, { propertyName: "ngControlHost", first: true, predicate: NgControl, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "formField", first: true, predicate: FormField, descendants: true, isSignal: true }, { propertyName: "formFieldHost", first: true, predicate: FormField, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "allControls", predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "@if (label()) {\n <label class=\"nxe-form-field-label\" [id]=\"labelId\" [attr.for]=\"controlId()\">\n {{ label() }}\n @if (isRequired()) {\n <span class=\"nxe-form-field-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n}\n<div class=\"nxe-form-field-control\">\n <ng-content />\n</div>\n@if (showHint()) {\n <div class=\"nxe-form-field-hint\" [id]=\"hintId\">{{ hint() }}</div>\n}\n<div class=\"nxe-form-field-messages\" aria-live=\"polite\">\n @if (invalid()) {\n <div class=\"nxe-form-field-error\" [id]=\"errorId\">\n @for (message of messages(); track $index) {\n <div>{{ message }}</div>\n }\n </div>\n }\n</div>\n", styles: ["nxe-form-field{display:block;min-width:0}nxe-form-field .nxe-form-field-label{display:block;margin-bottom:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-sm, 13px);font-weight:var(--nxe-ui-font-weight-medium, 500);line-height:var(--nxe-ui-line-height-sm, 18px)}nxe-form-field .nxe-form-field-required{margin-inline-start:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-hint,nxe-form-field .nxe-form-field-error{margin-top:var(--nxe-ui-space-1, 4px);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:var(--nxe-ui-line-height-xs, 16px)}nxe-form-field .nxe-form-field-hint{color:color-mix(in srgb,var(--nxe-ui-text, #333) 70%,var(--nxe-ui-surface, #fff))}nxe-form-field .nxe-form-field-error{color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-control .nxe-invalid-message{display:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2969
+ }
2970
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFormFieldComponent, decorators: [{
2971
+ type: Component,
2972
+ args: [{ selector: 'nxe-form-field', host: {
2973
+ class: 'nxe-form-field',
2974
+ '[class.nxe-form-field-invalid]': 'invalid()',
2975
+ }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (label()) {\n <label class=\"nxe-form-field-label\" [id]=\"labelId\" [attr.for]=\"controlId()\">\n {{ label() }}\n @if (isRequired()) {\n <span class=\"nxe-form-field-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n}\n<div class=\"nxe-form-field-control\">\n <ng-content />\n</div>\n@if (showHint()) {\n <div class=\"nxe-form-field-hint\" [id]=\"hintId\">{{ hint() }}</div>\n}\n<div class=\"nxe-form-field-messages\" aria-live=\"polite\">\n @if (invalid()) {\n <div class=\"nxe-form-field-error\" [id]=\"errorId\">\n @for (message of messages(); track $index) {\n <div>{{ message }}</div>\n }\n </div>\n }\n</div>\n", styles: ["nxe-form-field{display:block;min-width:0}nxe-form-field .nxe-form-field-label{display:block;margin-bottom:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-sm, 13px);font-weight:var(--nxe-ui-font-weight-medium, 500);line-height:var(--nxe-ui-line-height-sm, 18px)}nxe-form-field .nxe-form-field-required{margin-inline-start:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-hint,nxe-form-field .nxe-form-field-error{margin-top:var(--nxe-ui-space-1, 4px);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:var(--nxe-ui-line-height-xs, 16px)}nxe-form-field .nxe-form-field-hint{color:color-mix(in srgb,var(--nxe-ui-text, #333) 70%,var(--nxe-ui-surface, #fff))}nxe-form-field .nxe-form-field-error{color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-control .nxe-invalid-message{display:none}\n"] }]
2976
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], serverErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverErrors", required: false }] }], serverError: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverError", required: false }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], ngControl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }], ngControlHost: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { ...{ read: ElementRef }, isSignal: true }] }], formField: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FormField), { isSignal: true }] }], formFieldHost: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FormField), { ...{ read: ElementRef }, isSignal: true }] }], allControls: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NgControl), { ...{ descendants: true }, isSignal: true }] }] } });
2977
+
2978
+ /**
2979
+ * Marks the `<ng-template>` that renders one radio's content in `nxe-radio-group`, in place of the
2980
+ * `displayExpr` text:
2981
+ *
2982
+ * ```html
2983
+ * <nxe-radio-group [items]="plans" valueExpr="id">
2984
+ * <ng-template nxeRadioItem let-plan let-selected="selected">
2985
+ * <strong>{{ plan.name }}</strong> <small>{{ plan.price }}</small>
2986
+ * </ng-template>
2987
+ * </nxe-radio-group>
2988
+ * ```
2989
+ *
2990
+ * The radio's accessible name is the template's text, so keep it to text and inline markup.
2991
+ */
2992
+ class NxeRadioItemDirective {
2993
+ /** The template to render for each item. */
2994
+ template = inject(TemplateRef);
2995
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2996
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: NxeRadioItemDirective, isStandalone: true, selector: "ng-template[nxeRadioItem]", ngImport: i0 });
2997
+ }
2998
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioItemDirective, decorators: [{
2999
+ type: Directive,
3000
+ args: [{
3001
+ selector: 'ng-template[nxeRadioItem]',
3002
+ }]
3003
+ }] });
3004
+
3005
+ /**
3006
+ * `nxe-radio-group`: single choice among a few options, as the WAI-ARIA radio group pattern.
3007
+ *
3008
+ * A signal-CVA editor (extends {@link NxeFormControlBase}) working with `[(value)]`, `[(ngModel)]`
3009
+ * and Reactive Forms. The options are `items`; `displayExpr` says what each shows (default: the
3010
+ * item itself, for a list of strings), `valueExpr` what it puts in the control (default: the item
3011
+ * itself) and `disabledExpr` (default `'disabled'`) which are not selectable. An
3012
+ * `<ng-template nxeRadioItem>` replaces the text with your own content.
3013
+ *
3014
+ * Keyboard: the group is one tab stop, on the selected radio (or the first enabled one). Arrow keys
3015
+ * move to the next or previous enabled radio, wrap around, and select it; Space selects the focused
3016
+ * one. Left and Right are swapped in right-to-left text. A read-only group moves focus but never
3017
+ * changes the value.
3018
+ *
3019
+ * The inner `role="radiogroup"` element carries the `inputId`, `aria-label` and `aria-labelledby`,
3020
+ * so an `nxe-form-field` label names the group and its `aria-invalid` / `aria-describedby` land on
3021
+ * it. Provides {@link NXE_VALIDATION_TARGET} for a projected `nxe-validator`.
3022
+ */
3023
+ class NxeRadioGroupComponent extends NxeFormControlBase {
3024
+ /** The options. */
3025
+ items = input([], /* @ts-ignore */
3026
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
3027
+ /** Text of an item; unset, the item itself. */
3028
+ displayExpr = input(undefined, /* @ts-ignore */
3029
+ ...(ngDevMode ? [{ debugName: "displayExpr" }] : /* istanbul ignore next */ []));
3030
+ /** The value an item stands for; unset, the item itself. */
3031
+ valueExpr = input(undefined, /* @ts-ignore */
3032
+ ...(ngDevMode ? [{ debugName: "valueExpr" }] : /* istanbul ignore next */ []));
3033
+ /** Truthy for an item that cannot be chosen. */
3034
+ disabledExpr = input('disabled', /* @ts-ignore */
3035
+ ...(ngDevMode ? [{ debugName: "disabledExpr" }] : /* istanbul ignore next */ []));
3036
+ /** `vertical` stacks the radios, `horizontal` puts them in a row that wraps. */
3037
+ layout = input('vertical', /* @ts-ignore */
3038
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
3039
+ /** Custom radio content. */
3040
+ itemTemplate = contentChild(NxeRadioItemDirective, /* @ts-ignore */
3041
+ ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
3042
+ entries = computed(() => this.items().map((item, index) => ({
3043
+ item,
3044
+ index,
3045
+ id: `${this.controlId()}-radio-${index}`,
3046
+ value: nxeResolveExpr(this.valueExpr(), item),
3047
+ text: nxeResolveText(this.displayExpr(), item),
3048
+ disabled: !!nxeResolveExpr(this.disabledExpr(), item),
3049
+ })), /* @ts-ignore */
3050
+ ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
3051
+ /** Index of the checked radio, or -1. */
3052
+ selectedIndex = computed(() => {
3053
+ const current = this.value();
3054
+ return this.entries().findIndex((entry) => Object.is(entry.value, current));
3055
+ }, /* @ts-ignore */
3056
+ ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
3057
+ /** The one radio in the tab order: the checked one when it can be reached, else the first enabled. */
3058
+ tabbableIndex = computed(() => {
3059
+ if (this.disabled()) {
3060
+ return -1;
3061
+ }
3062
+ const entries = this.entries();
3063
+ const selected = this.selectedIndex();
3064
+ if (selected >= 0 && !entries[selected].disabled) {
3065
+ return selected;
3066
+ }
3067
+ return entries.findIndex((entry) => !entry.disabled);
3068
+ }, /* @ts-ignore */
3069
+ ...(ngDevMode ? [{ debugName: "tabbableIndex" }] : /* istanbul ignore next */ []));
3070
+ onItemClick(index, event) {
3071
+ this.select(index, event);
3072
+ }
3073
+ onItemKeydown(index, event) {
3074
+ if (event.altKey || event.ctrlKey || event.metaKey) {
3075
+ return;
3076
+ }
3077
+ const rtl = getComputedStyle(this.element).direction === 'rtl';
3078
+ switch (event.key) {
3079
+ case 'ArrowDown':
3080
+ this.move(index, 1, event);
3081
+ break;
3082
+ case 'ArrowUp':
3083
+ this.move(index, -1, event);
3084
+ break;
3085
+ case 'ArrowRight':
3086
+ this.move(index, rtl ? -1 : 1, event);
3087
+ break;
3088
+ case 'ArrowLeft':
3089
+ this.move(index, rtl ? 1 : -1, event);
3090
+ break;
3091
+ case ' ':
3092
+ event.preventDefault();
3093
+ this.select(index, event);
3094
+ break;
3095
+ }
3096
+ }
3097
+ /** Leaving the group, not moving between its radios, is what touches the control. */
3098
+ onGroupFocusOut(event) {
3099
+ const group = event.currentTarget;
3100
+ if (!group.contains(event.relatedTarget)) {
3101
+ this.markAsTouched();
3102
+ }
3103
+ }
3104
+ /** Focus (and, unless read-only, select) the next enabled radio in `step` direction, wrapping. */
3105
+ move(from, step, event) {
3106
+ event.preventDefault();
3107
+ if (this.disabled()) {
3108
+ return;
3109
+ }
3110
+ const entries = this.entries();
3111
+ for (let n = 1; n <= entries.length; n++) {
3112
+ const next = (((from + step * n) % entries.length) + entries.length) % entries.length;
3113
+ if (!entries[next].disabled) {
3114
+ this.element.querySelectorAll('[role="radio"]')[next]?.focus();
3115
+ this.select(next, event);
3116
+ return;
3117
+ }
3118
+ }
3119
+ }
3120
+ select(index, event) {
3121
+ const entry = this.entries()[index];
3122
+ if (!entry || entry.disabled || this.disabled() || this.readOnly()) {
3123
+ return;
3124
+ }
3125
+ this.commitValue(entry.value, event);
3126
+ this.markAsTouched();
3127
+ }
3128
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioGroupComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3129
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeRadioGroupComponent, isStandalone: true, selector: "nxe-radio-group", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, displayExpr: { classPropertyName: "displayExpr", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExpr: { classPropertyName: "valueExpr", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, disabledExpr: { classPropertyName: "disabledExpr", publicName: "disabledExpr", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-radio-group-horizontal": "layout() === 'horizontal'", "class.nxe-radio-group-vertical": "layout() === 'vertical'", "class.nxe-invalid": "invalidState()", "attr.aria-label": "null", "attr.aria-labelledby": "null" }, classAttribute: "nxe-widget nxe-radio-group" }, providers: [
3130
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeRadioGroupComponent), multi: true },
3131
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeRadioGroupComponent) },
3132
+ ], queries: [{ propertyName: "itemTemplate", first: true, predicate: NxeRadioItemDirective, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"nxe-radio-group-items\"\n role=\"radiogroup\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-disabled]=\"disabled() ? true : null\"\n [attr.aria-readonly]=\"readOnly() ? true : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (focusout)=\"onGroupFocusOut($event)\">\n @for (entry of entries(); track entry.index) {\n <div\n class=\"nxe-radio\"\n role=\"radio\"\n [id]=\"entry.id\"\n [class.nxe-radio-checked]=\"selectedIndex() === entry.index\"\n [class.nxe-radio-disabled]=\"entry.disabled\"\n [attr.aria-checked]=\"selectedIndex() === entry.index\"\n [attr.aria-disabled]=\"entry.disabled || disabled() ? true : null\"\n [attr.tabindex]=\"tabbableIndex() === entry.index ? 0 : -1\"\n (click)=\"onItemClick(entry.index, $event)\"\n (keydown)=\"onItemKeydown(entry.index, $event)\">\n <span class=\"nxe-radio-icon\" aria-hidden=\"true\"></span>\n <span class=\"nxe-radio-label\">\n @if (itemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: entry.item,\n item: entry.item,\n index: entry.index,\n selected: selectedIndex() === entry.index,\n disabled: entry.disabled,\n }\" />\n } @else {\n {{ entry.text }}\n }\n </span>\n </div>\n }\n</div>\n<ng-content />\n", styles: ["nxe-radio-group{display:block;min-width:0}nxe-radio-group .nxe-radio-group-items{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2, 8px)}nxe-radio-group.nxe-radio-group-horizontal .nxe-radio-group-items{flex-flow:row wrap;gap:var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-5, 20px)}nxe-radio-group .nxe-radio{display:inline-flex;align-items:flex-start;gap:var(--nxe-ui-space-2, 8px);align-self:flex-start;max-width:100%;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:var(--nxe-ui-line-height, 1.35715)}nxe-radio-group .nxe-radio:focus{outline:none}nxe-radio-group .nxe-radio-icon{position:relative;box-sizing:border-box;flex-shrink:0;margin-block-start:1px;width:18px;height:18px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:50%;background-color:var(--nxe-ui-surface, #fff)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-radio-group .nxe-radio-label{min-width:0;overflow-wrap:anywhere}nxe-radio-group .nxe-radio:not(.nxe-radio-disabled):hover .nxe-radio-icon{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-radio-group .nxe-radio:focus-visible .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-radio-group .nxe-radio-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-disabled .nxe-radio{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-readonly .nxe-radio{cursor:default}nxe-radio-group.nxe-invalid .nxe-radio-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3133
+ }
3134
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioGroupComponent, decorators: [{
3135
+ type: Component,
3136
+ args: [{ selector: 'nxe-radio-group', imports: [NgTemplateOutlet], host: {
3137
+ class: 'nxe-widget nxe-radio-group',
3138
+ '[class.nxe-radio-group-horizontal]': "layout() === 'horizontal'",
3139
+ '[class.nxe-radio-group-vertical]': "layout() === 'vertical'",
3140
+ '[class.nxe-invalid]': 'invalidState()',
3141
+ // The names belong on the inner radiogroup; a copy on this generic host would be prohibited.
3142
+ '[attr.aria-label]': 'null',
3143
+ '[attr.aria-labelledby]': 'null',
3144
+ }, providers: [
3145
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeRadioGroupComponent), multi: true },
3146
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeRadioGroupComponent) },
3147
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div\n class=\"nxe-radio-group-items\"\n role=\"radiogroup\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-disabled]=\"disabled() ? true : null\"\n [attr.aria-readonly]=\"readOnly() ? true : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (focusout)=\"onGroupFocusOut($event)\">\n @for (entry of entries(); track entry.index) {\n <div\n class=\"nxe-radio\"\n role=\"radio\"\n [id]=\"entry.id\"\n [class.nxe-radio-checked]=\"selectedIndex() === entry.index\"\n [class.nxe-radio-disabled]=\"entry.disabled\"\n [attr.aria-checked]=\"selectedIndex() === entry.index\"\n [attr.aria-disabled]=\"entry.disabled || disabled() ? true : null\"\n [attr.tabindex]=\"tabbableIndex() === entry.index ? 0 : -1\"\n (click)=\"onItemClick(entry.index, $event)\"\n (keydown)=\"onItemKeydown(entry.index, $event)\">\n <span class=\"nxe-radio-icon\" aria-hidden=\"true\"></span>\n <span class=\"nxe-radio-label\">\n @if (itemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: entry.item,\n item: entry.item,\n index: entry.index,\n selected: selectedIndex() === entry.index,\n disabled: entry.disabled,\n }\" />\n } @else {\n {{ entry.text }}\n }\n </span>\n </div>\n }\n</div>\n<ng-content />\n", styles: ["nxe-radio-group{display:block;min-width:0}nxe-radio-group .nxe-radio-group-items{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2, 8px)}nxe-radio-group.nxe-radio-group-horizontal .nxe-radio-group-items{flex-flow:row wrap;gap:var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-5, 20px)}nxe-radio-group .nxe-radio{display:inline-flex;align-items:flex-start;gap:var(--nxe-ui-space-2, 8px);align-self:flex-start;max-width:100%;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:var(--nxe-ui-line-height, 1.35715)}nxe-radio-group .nxe-radio:focus{outline:none}nxe-radio-group .nxe-radio-icon{position:relative;box-sizing:border-box;flex-shrink:0;margin-block-start:1px;width:18px;height:18px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:50%;background-color:var(--nxe-ui-surface, #fff)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-radio-group .nxe-radio-label{min-width:0;overflow-wrap:anywhere}nxe-radio-group .nxe-radio:not(.nxe-radio-disabled):hover .nxe-radio-icon{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-radio-group .nxe-radio:focus-visible .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-radio-group .nxe-radio-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-disabled .nxe-radio{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-readonly .nxe-radio{cursor:default}nxe-radio-group.nxe-invalid .nxe-radio-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"] }]
3148
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], displayExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], valueExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], disabledExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledExpr", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], itemTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeRadioItemDirective), { isSignal: true }] }] } });
3149
+
3150
+ /**
3151
+ * `nxe-switch`: an on/off setting that takes effect as soon as it is flipped.
3152
+ *
3153
+ * A signal-CVA editor (extends {@link NxeFormControlBase}) over `boolean`; `null` and unset read as
3154
+ * off. Works with `[(value)]`, `[(ngModel)]` and Reactive Forms. The host is the control:
3155
+ * `role="switch"`, `aria-checked`, one tab stop; click and Space toggle it. A read-only switch is
3156
+ * focusable but never flips; a disabled one leaves the tab order and dims.
3157
+ *
3158
+ * `text` is the label beside the track and names the switch (inside an `nxe-form-field` the field's
3159
+ * label does instead). `onText` and `offText` are optional words drawn inside the track; they are
3160
+ * decoration, hidden from assistive tech, because `aria-checked` already says which state it is in.
3161
+ */
3162
+ class NxeSwitchComponent extends NxeFormControlBase {
3163
+ /** Label beside the track. */
3164
+ text = input(undefined, /* @ts-ignore */
3165
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
3166
+ /** Word shown inside the track while on. */
3167
+ onText = input(undefined, /* @ts-ignore */
3168
+ ...(ngDevMode ? [{ debugName: "onText" }] : /* istanbul ignore next */ []));
3169
+ /** Word shown inside the track while off. */
3170
+ offText = input(undefined, /* @ts-ignore */
3171
+ ...(ngDevMode ? [{ debugName: "offText" }] : /* istanbul ignore next */ []));
3172
+ /** Track size. */
3173
+ size = input('medium', /* @ts-ignore */
3174
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3175
+ checked = computed(() => !!this.value(), /* @ts-ignore */
3176
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
3177
+ toggle(event) {
3178
+ if (this.disabled() || this.readOnly()) {
3179
+ return;
3180
+ }
3181
+ this.commitValue(!this.checked(), event);
3182
+ this.markAsTouched();
3183
+ }
3184
+ onSpace(event) {
3185
+ // Space would otherwise scroll the page.
3186
+ event.preventDefault();
3187
+ this.toggle(event);
3188
+ }
3189
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSwitchComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3190
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeSwitchComponent, isStandalone: true, selector: "nxe-switch", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, onText: { classPropertyName: "onText", publicName: "onText", isSignal: true, isRequired: false, transformFunction: null }, offText: { classPropertyName: "offText", publicName: "offText", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "switch" }, listeners: { "click": "toggle($event)", "keydown.space": "onSpace($event)", "blur": "markAsTouched()" }, properties: { "attr.aria-checked": "checked()", "attr.aria-disabled": "disabled() ? true : null", "attr.aria-readonly": "readOnly() ? true : null", "attr.aria-invalid": "invalidState() ? true : null", "attr.aria-required": "required() ? true : null", "attr.id": "inputId() ? controlId() : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledBy() || null", "attr.tabindex": "disabled() ? null : (tabIndex() ?? 0)", "class.nxe-switch-checked": "checked()", "class.nxe-switch-small": "size() === 'small'", "class.nxe-switch-large": "size() === 'large'", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-switch" }, providers: [
3191
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeSwitchComponent), multi: true },
3192
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeSwitchComponent) },
3193
+ ], usesInheritance: true, ngImport: i0, template: "<span class=\"nxe-switch-track\" aria-hidden=\"true\">\n @if (onText() || offText()) {\n <span class=\"nxe-switch-text nxe-switch-on-text\">{{ onText() }}</span>\n <span class=\"nxe-switch-text nxe-switch-off-text\">{{ offText() }}</span>\n }\n <span class=\"nxe-switch-thumb\"></span>\n</span>\n@if (text()) {\n <span class=\"nxe-switch-label\">{{ text() }}</span>\n}\n<ng-content />\n", styles: ["nxe-switch{--_track-w: var(--nxe-switch-track-w, 40px);--_track-h: var(--nxe-switch-track-h, 22px);--_thumb: var(--nxe-switch-thumb, 16px);--_pad: calc((var(--_track-h) - var(--_thumb)) / 2);display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2, 8px);vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:normal}nxe-switch:focus{outline:none}nxe-switch.nxe-switch-small{--_track-w: var(--nxe-switch-track-w, 30px);--_track-h: var(--nxe-switch-track-h, 18px);--_thumb: var(--nxe-switch-thumb, 12px)}nxe-switch.nxe-switch-large{--_track-w: var(--nxe-switch-track-w, 52px);--_track-h: var(--nxe-switch-track-h, 28px);--_thumb: var(--nxe-switch-thumb, 22px)}nxe-switch .nxe-switch-track{position:relative;display:inline-grid;flex-shrink:0;align-items:center;box-sizing:border-box;min-width:var(--_track-w);height:var(--_track-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius-pill, 999px);background-color:var(--nxe-ui-neutral-80, #f4f4f4);font-size:var(--nxe-ui-font-size-xs, 12px);font-weight:var(--nxe-ui-font-weight-medium, 500);color:var(--nxe-ui-text, #333);white-space:nowrap}nxe-switch .nxe-switch-text{grid-area:1/1;text-align:center;padding-inline:var(--_pad)}nxe-switch .nxe-switch-on-text{padding-inline-end:calc(var(--_thumb) + 2 * var(--_pad));visibility:hidden}nxe-switch .nxe-switch-off-text{padding-inline-start:calc(var(--_thumb) + 2 * var(--_pad))}nxe-switch .nxe-switch-thumb{position:absolute;top:50%;inset-inline-start:calc(var(--_pad) - 1px);width:var(--_thumb);height:var(--_thumb);margin-block-start:calc(var(--_thumb) / -2);border-radius:50%;background-color:var(--nxe-ui-surface, #fff);box-shadow:0 1px 2px #0000004d}@media(prefers-reduced-motion:no-preference){nxe-switch .nxe-switch-thumb{transition:inset-inline-start .15s ease}}nxe-switch.nxe-switch-checked .nxe-switch-track{border-color:var(--nxe-ui-accent, #337ab7);background-color:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff)}nxe-switch.nxe-switch-checked .nxe-switch-thumb{inset-inline-start:calc(100% - var(--_thumb) - var(--_pad) + 1px)}nxe-switch.nxe-switch-checked .nxe-switch-on-text{visibility:visible}nxe-switch.nxe-switch-checked .nxe-switch-off-text{visibility:hidden}nxe-switch:not(.nxe-disabled):not(.nxe-switch-checked):hover .nxe-switch-track{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-switch:focus-visible .nxe-switch-track{box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-switch.nxe-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-switch.nxe-readonly{cursor:default}nxe-switch.nxe-invalid .nxe-switch-track{border-color:var(--nxe-ui-invalid, #d9534f)}@media(forced-colors:active){nxe-switch .nxe-switch-track{forced-color-adjust:none;border-color:CanvasText;background-color:Canvas;color:CanvasText}nxe-switch .nxe-switch-thumb{background-color:CanvasText}nxe-switch.nxe-switch-checked .nxe-switch-track{background-color:Highlight;color:HighlightText}nxe-switch.nxe-switch-checked .nxe-switch-thumb{background-color:HighlightText}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3194
+ }
3195
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSwitchComponent, decorators: [{
3196
+ type: Component,
3197
+ args: [{ selector: 'nxe-switch', host: {
3198
+ class: 'nxe-widget nxe-switch',
3199
+ role: 'switch',
3200
+ '[attr.aria-checked]': 'checked()',
3201
+ '[attr.aria-disabled]': 'disabled() ? true : null',
3202
+ '[attr.aria-readonly]': 'readOnly() ? true : null',
3203
+ '[attr.aria-invalid]': 'invalidState() ? true : null',
3204
+ '[attr.aria-required]': 'required() ? true : null',
3205
+ // The host IS the control, so the id belongs on it rather than on a child.
3206
+ '[attr.id]': 'inputId() ? controlId() : null',
3207
+ '[attr.aria-label]': 'ariaLabel() || null',
3208
+ '[attr.aria-labelledby]': 'ariaLabelledBy() || null',
3209
+ '[attr.tabindex]': 'disabled() ? null : (tabIndex() ?? 0)',
3210
+ '[class.nxe-switch-checked]': 'checked()',
3211
+ '[class.nxe-switch-small]': "size() === 'small'",
3212
+ '[class.nxe-switch-large]': "size() === 'large'",
3213
+ '[class.nxe-invalid]': 'invalidState()',
3214
+ '(click)': 'toggle($event)',
3215
+ '(keydown.space)': 'onSpace($event)',
3216
+ '(blur)': 'markAsTouched()',
3217
+ }, providers: [
3218
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeSwitchComponent), multi: true },
3219
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeSwitchComponent) },
3220
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<span class=\"nxe-switch-track\" aria-hidden=\"true\">\n @if (onText() || offText()) {\n <span class=\"nxe-switch-text nxe-switch-on-text\">{{ onText() }}</span>\n <span class=\"nxe-switch-text nxe-switch-off-text\">{{ offText() }}</span>\n }\n <span class=\"nxe-switch-thumb\"></span>\n</span>\n@if (text()) {\n <span class=\"nxe-switch-label\">{{ text() }}</span>\n}\n<ng-content />\n", styles: ["nxe-switch{--_track-w: var(--nxe-switch-track-w, 40px);--_track-h: var(--nxe-switch-track-h, 22px);--_thumb: var(--nxe-switch-thumb, 16px);--_pad: calc((var(--_track-h) - var(--_thumb)) / 2);display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2, 8px);vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:normal}nxe-switch:focus{outline:none}nxe-switch.nxe-switch-small{--_track-w: var(--nxe-switch-track-w, 30px);--_track-h: var(--nxe-switch-track-h, 18px);--_thumb: var(--nxe-switch-thumb, 12px)}nxe-switch.nxe-switch-large{--_track-w: var(--nxe-switch-track-w, 52px);--_track-h: var(--nxe-switch-track-h, 28px);--_thumb: var(--nxe-switch-thumb, 22px)}nxe-switch .nxe-switch-track{position:relative;display:inline-grid;flex-shrink:0;align-items:center;box-sizing:border-box;min-width:var(--_track-w);height:var(--_track-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius-pill, 999px);background-color:var(--nxe-ui-neutral-80, #f4f4f4);font-size:var(--nxe-ui-font-size-xs, 12px);font-weight:var(--nxe-ui-font-weight-medium, 500);color:var(--nxe-ui-text, #333);white-space:nowrap}nxe-switch .nxe-switch-text{grid-area:1/1;text-align:center;padding-inline:var(--_pad)}nxe-switch .nxe-switch-on-text{padding-inline-end:calc(var(--_thumb) + 2 * var(--_pad));visibility:hidden}nxe-switch .nxe-switch-off-text{padding-inline-start:calc(var(--_thumb) + 2 * var(--_pad))}nxe-switch .nxe-switch-thumb{position:absolute;top:50%;inset-inline-start:calc(var(--_pad) - 1px);width:var(--_thumb);height:var(--_thumb);margin-block-start:calc(var(--_thumb) / -2);border-radius:50%;background-color:var(--nxe-ui-surface, #fff);box-shadow:0 1px 2px #0000004d}@media(prefers-reduced-motion:no-preference){nxe-switch .nxe-switch-thumb{transition:inset-inline-start .15s ease}}nxe-switch.nxe-switch-checked .nxe-switch-track{border-color:var(--nxe-ui-accent, #337ab7);background-color:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff)}nxe-switch.nxe-switch-checked .nxe-switch-thumb{inset-inline-start:calc(100% - var(--_thumb) - var(--_pad) + 1px)}nxe-switch.nxe-switch-checked .nxe-switch-on-text{visibility:visible}nxe-switch.nxe-switch-checked .nxe-switch-off-text{visibility:hidden}nxe-switch:not(.nxe-disabled):not(.nxe-switch-checked):hover .nxe-switch-track{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-switch:focus-visible .nxe-switch-track{box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-switch.nxe-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-switch.nxe-readonly{cursor:default}nxe-switch.nxe-invalid .nxe-switch-track{border-color:var(--nxe-ui-invalid, #d9534f)}@media(forced-colors:active){nxe-switch .nxe-switch-track{forced-color-adjust:none;border-color:CanvasText;background-color:Canvas;color:CanvasText}nxe-switch .nxe-switch-thumb{background-color:CanvasText}nxe-switch.nxe-switch-checked .nxe-switch-track{background-color:Highlight;color:HighlightText}nxe-switch.nxe-switch-checked .nxe-switch-thumb{background-color:HighlightText}}\n"] }]
3221
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], onText: [{ type: i0.Input, args: [{ isSignal: true, alias: "onText", required: false }] }], offText: [{ type: i0.Input, args: [{ isSignal: true, alias: "offText", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3222
+
3223
+ const STRIP = {
3224
+ numeric: /\D/g,
3225
+ alphanumeric: /[^A-Za-z0-9]/g,
3226
+ };
3227
+ /**
3228
+ * `nxe-otp-box`: a one-time code (2FA sign-in, e-mail verification, recovery code) drawn as one
3229
+ * cell per character.
3230
+ *
3231
+ * It is ONE control, not N: a single transparent `<input>` lies over the cells and they only draw
3232
+ * its text. That is what keeps a screen reader on one named field with one value, gives native
3233
+ * paste, and lets the browser fill the whole code at once from an SMS or password manager
3234
+ * (`autocomplete="one-time-code"`). Typing fills the next cell, Backspace empties the last one, and
3235
+ * the caret is kept at the end, so the arrow keys do nothing. Pasting a code of `length` characters
3236
+ * (or more) replaces the value, with separators such as a dash or space dropped; a shorter paste is
3237
+ * appended.
3238
+ *
3239
+ * A signal-CVA over `string` (extends {@link NxeFormControlBase}); the value follows every
3240
+ * keystroke, so a `minLength` validator sees a partial code and `completed` fires once with the full
3241
+ * code. Name it with `aria-label`, `aria-labelledby` or an `nxe-form-field` label.
3242
+ */
3243
+ class NxeOtpBoxComponent extends NxeFormControlBase {
3244
+ /** Number of characters in the code. */
3245
+ length = input(6, { ...(ngDevMode ? { debugName: "length" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(1, Math.floor(numberAttribute(v, 6))) });
3246
+ /** `numeric` (default) takes digits and opens the numeric keypad; `alphanumeric` takes letters and digits. */
3247
+ mode = input('numeric', /* @ts-ignore */
3248
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
3249
+ /** Focus the field when it first renders. */
3250
+ autoFocus = input(false, { ...(ngDevMode ? { debugName: "autoFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3251
+ /** The full code, once, when the last cell is filled (typed, pasted or auto-filled). */
3252
+ completed = output();
3253
+ focused = signal(false, /* @ts-ignore */
3254
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
3255
+ field = viewChild.required('field', /* @ts-ignore */
3256
+ ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
3257
+ displayValue = computed(() => this.clean(this.value() ?? ''), /* @ts-ignore */
3258
+ ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
3259
+ cells = computed(() => {
3260
+ const text = this.displayValue();
3261
+ const length = this.length();
3262
+ // The cell the next character lands in, or the last one while the code is full.
3263
+ const active = this.focused() && !this.disabled() ? Math.min(text.length, length - 1) : -1;
3264
+ return Array.from({ length }, (_, index) => ({ index, char: text[index] ?? '', active: index === active }));
3265
+ }, /* @ts-ignore */
3266
+ ...(ngDevMode ? [{ debugName: "cells" }] : /* istanbul ignore next */ []));
3267
+ constructor() {
3268
+ super();
3269
+ afterNextRender(() => {
3270
+ if (this.autoFocus()) {
3271
+ this.field().nativeElement.focus();
3272
+ }
3273
+ });
3274
+ }
3275
+ /** Drop what the mode does not allow and cut to `length`. */
3276
+ clean(text) {
3277
+ return text.replace(STRIP[this.mode()], '').slice(0, this.length());
3278
+ }
3279
+ /** Typing, deleting and autofill: the field's text is the new value. */
3280
+ handleInput(event) {
3281
+ const el = event.target;
3282
+ const text = this.clean(el.value);
3283
+ // A rejected character or an overflowing one leaves the model unchanged, so the field has to be put right by hand.
3284
+ if (el.value !== text) {
3285
+ el.value = text;
3286
+ }
3287
+ this.apply(text, event);
3288
+ this.keepCaretAtEnd();
3289
+ }
3290
+ /** A whole code replaces the value; a fragment is appended. */
3291
+ onPaste(event) {
3292
+ event.preventDefault();
3293
+ if (this.readOnly() || this.disabled()) {
3294
+ return;
3295
+ }
3296
+ const pasted = (event.clipboardData?.getData('text') ?? '').replace(STRIP[this.mode()], '');
3297
+ if (!pasted) {
3298
+ return;
3299
+ }
3300
+ const el = event.target;
3301
+ const next = this.clean(pasted.length >= this.length() ? pasted : el.value + pasted);
3302
+ el.value = next;
3303
+ this.apply(next, event);
3304
+ this.keepCaretAtEnd();
3305
+ }
3306
+ /** Keys that would move the caret off the end do nothing: the cells have no caret to move. */
3307
+ onKeydown(event) {
3308
+ if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
3309
+ event.preventDefault();
3310
+ }
3311
+ }
3312
+ onFocus() {
3313
+ this.focused.set(true);
3314
+ this.keepCaretAtEnd();
3315
+ }
3316
+ onBlur() {
3317
+ this.focused.set(false);
3318
+ this.markAsTouched();
3319
+ }
3320
+ /** A click or a drag could put the caret mid-code or select it; it always belongs at the end. */
3321
+ keepCaretAtEnd() {
3322
+ const el = this.field().nativeElement;
3323
+ const end = el.value.length;
3324
+ if (el.selectionStart !== end || el.selectionEnd !== end) {
3325
+ el.setSelectionRange(end, end);
3326
+ }
3327
+ }
3328
+ apply(text, event) {
3329
+ const before = this.value() ?? '';
3330
+ this.commitValue(text, event);
3331
+ if (text.length === this.length() && text !== before) {
3332
+ this.completed.emit(text);
3333
+ }
3334
+ }
3335
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeOtpBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3336
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeOtpBoxComponent, isStandalone: true, selector: "nxe-otp-box", inputs: { length: { classPropertyName: "length", publicName: "length", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, autoFocus: { classPropertyName: "autoFocus", publicName: "autoFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { completed: "completed" }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()", "attr.aria-label": "null", "attr.aria-labelledby": "null" }, classAttribute: "nxe-widget nxe-otp-box" }, providers: [
3337
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeOtpBoxComponent), multi: true },
3338
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeOtpBoxComponent) },
3339
+ ], viewQueries: [{ propertyName: "field", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-otp-cells\" aria-hidden=\"true\">\n @for (cell of cells(); track cell.index) {\n <span\n class=\"nxe-otp-cell\"\n [class.nxe-otp-cell-filled]=\"cell.char !== ''\"\n [class.nxe-otp-cell-active]=\"cell.active\">{{ cell.char }}</span>\n }\n</div>\n<!--\n One real input sits over the cells, so the code is a single control: one tab stop, one name, one\n value for a screen reader, native paste and the one-time-code autofill. The cells only draw it.\n-->\n<input\n #field\n class=\"nxe-otp-input\"\n type=\"text\"\n autocomplete=\"one-time-code\"\n autocapitalize=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n [id]=\"controlId()\"\n [attr.inputmode]=\"mode() === 'numeric' ? 'numeric' : 'text'\"\n [attr.pattern]=\"mode() === 'numeric' ? '[0-9]*' : null\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [value]=\"displayValue()\"\n [readOnly]=\"readOnly()\"\n [disabled]=\"disabled()\"\n (input)=\"handleInput($event)\"\n (paste)=\"onPaste($event)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus()\"\n (click)=\"keepCaretAtEnd()\"\n (select)=\"keepCaretAtEnd()\"\n (blur)=\"onBlur()\" />\n<ng-content />\n", styles: ["nxe-otp-box{--_cell-w: var(--nxe-otp-cell-w, 40px);--_cell-h: var(--nxe-otp-cell-h, 48px);--_gap: var(--nxe-otp-gap, 8px);position:relative;display:inline-block;max-width:100%;vertical-align:middle}nxe-otp-box .nxe-otp-cells{display:flex;gap:var(--_gap)}nxe-otp-box .nxe-otp-cell{box-sizing:border-box;display:inline-flex;flex:0 0 var(--_cell-w);align-items:center;justify-content:center;width:var(--_cell-w);height:var(--_cell-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-xl, 20px);font-weight:var(--nxe-ui-font-weight-medium, 500);font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none}nxe-otp-box .nxe-otp-input{position:absolute;inset:0;box-sizing:border-box;width:100%;height:100%;margin:0;padding:0;border:0;outline:0;background:transparent;color:transparent;caret-color:transparent;font-size:16px;opacity:0;cursor:text}nxe-otp-box:hover .nxe-otp-cell{border-color:var(--nxe-ui-accent-faded, rgba(51, 122, 183, .4))}nxe-otp-box.nxe-focused .nxe-otp-cell-active{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-otp-box.nxe-invalid .nxe-otp-cell,nxe-otp-box.nxe-invalid:hover .nxe-otp-cell{border-color:var(--nxe-ui-invalid, #d9534f)}nxe-otp-box.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-otp-box.nxe-disabled .nxe-otp-input{cursor:default}nxe-otp-box.nxe-readonly .nxe-otp-cell{border-style:dashed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3340
+ }
3341
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeOtpBoxComponent, decorators: [{
3342
+ type: Component,
3343
+ args: [{ selector: 'nxe-otp-box', host: {
3344
+ class: 'nxe-widget nxe-otp-box',
3345
+ '[class.nxe-focused]': 'focused()',
3346
+ '[class.nxe-invalid]': 'invalidState()',
3347
+ // The names belong on the inner input; a copy on this generic host would be prohibited.
3348
+ '[attr.aria-label]': 'null',
3349
+ '[attr.aria-labelledby]': 'null',
3350
+ }, providers: [
3351
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeOtpBoxComponent), multi: true },
3352
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeOtpBoxComponent) },
3353
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-otp-cells\" aria-hidden=\"true\">\n @for (cell of cells(); track cell.index) {\n <span\n class=\"nxe-otp-cell\"\n [class.nxe-otp-cell-filled]=\"cell.char !== ''\"\n [class.nxe-otp-cell-active]=\"cell.active\">{{ cell.char }}</span>\n }\n</div>\n<!--\n One real input sits over the cells, so the code is a single control: one tab stop, one name, one\n value for a screen reader, native paste and the one-time-code autofill. The cells only draw it.\n-->\n<input\n #field\n class=\"nxe-otp-input\"\n type=\"text\"\n autocomplete=\"one-time-code\"\n autocapitalize=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n [id]=\"controlId()\"\n [attr.inputmode]=\"mode() === 'numeric' ? 'numeric' : 'text'\"\n [attr.pattern]=\"mode() === 'numeric' ? '[0-9]*' : null\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [value]=\"displayValue()\"\n [readOnly]=\"readOnly()\"\n [disabled]=\"disabled()\"\n (input)=\"handleInput($event)\"\n (paste)=\"onPaste($event)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus()\"\n (click)=\"keepCaretAtEnd()\"\n (select)=\"keepCaretAtEnd()\"\n (blur)=\"onBlur()\" />\n<ng-content />\n", styles: ["nxe-otp-box{--_cell-w: var(--nxe-otp-cell-w, 40px);--_cell-h: var(--nxe-otp-cell-h, 48px);--_gap: var(--nxe-otp-gap, 8px);position:relative;display:inline-block;max-width:100%;vertical-align:middle}nxe-otp-box .nxe-otp-cells{display:flex;gap:var(--_gap)}nxe-otp-box .nxe-otp-cell{box-sizing:border-box;display:inline-flex;flex:0 0 var(--_cell-w);align-items:center;justify-content:center;width:var(--_cell-w);height:var(--_cell-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-xl, 20px);font-weight:var(--nxe-ui-font-weight-medium, 500);font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none}nxe-otp-box .nxe-otp-input{position:absolute;inset:0;box-sizing:border-box;width:100%;height:100%;margin:0;padding:0;border:0;outline:0;background:transparent;color:transparent;caret-color:transparent;font-size:16px;opacity:0;cursor:text}nxe-otp-box:hover .nxe-otp-cell{border-color:var(--nxe-ui-accent-faded, rgba(51, 122, 183, .4))}nxe-otp-box.nxe-focused .nxe-otp-cell-active{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-otp-box.nxe-invalid .nxe-otp-cell,nxe-otp-box.nxe-invalid:hover .nxe-otp-cell{border-color:var(--nxe-ui-invalid, #d9534f)}nxe-otp-box.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-otp-box.nxe-disabled .nxe-otp-input{cursor:default}nxe-otp-box.nxe-readonly .nxe-otp-cell{border-style:dashed}\n"] }]
3354
+ }], ctorParameters: () => [], propDecorators: { length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], completed: [{ type: i0.Output, args: ["completed"] }], field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
3355
+
1106
3356
  // @nexterajs/ui/forms — public API.
1107
3357
 
1108
3358
  /**
1109
3359
  * Generated bundle index. Do not edit.
1110
3360
  */
1111
3361
 
1112
- export { NxeButtonComponent, NxeCheckBoxComponent, NxeDateBoxComponent, NxeNumberBoxComponent, NxeScrollViewComponent, NxeTextAreaComponent, NxeTextBoxComponent };
3362
+ export { NXE_DATE_RANGE_DISPLAY_FORMAT, NXE_DATE_RANGE_NOW, NXE_DATE_RANGE_VALUE_FORMAT, NXE_DEFAULT_ERROR_MESSAGES, NXE_ERROR_MESSAGES, NXE_FALLBACK_ERROR_KEY, NxeButtonComponent, NxeCheckBoxComponent, NxeDateBoxComponent, NxeDateRangeBoxComponent, NxeFormFieldComponent, NxeNumberBoxComponent, NxeOtpBoxComponent, NxeRadioGroupComponent, NxeRadioItemDirective, NxeScrollViewComponent, NxeSwitchComponent, NxeTextAreaComponent, NxeTextBoxComponent, nxeDateRangePresets, provideNxeErrorMessages };
1113
3363
  //# sourceMappingURL=nexterajs-ui-forms.mjs.map