@nexterajs/ui 0.0.2 → 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.
- package/README.md +279 -34
- package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
- package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-core.mjs +652 -12
- package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-display.mjs +1142 -0
- package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs +321 -33
- package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-file-uploader.mjs +572 -10
- package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-forms.mjs +2027 -136
- package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-grid.mjs +1222 -188
- package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-intl.mjs +103 -11
- package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
- package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-notify.mjs +46 -44
- package/fesm2022/nexterajs-ui-notify.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
- package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-pivot.mjs +51 -26
- package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-shell.mjs +542 -0
- package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs +5 -2
- package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-validation.mjs +42 -11
- package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
- package/fesm2022/nexterajs-ui.mjs +5 -0
- package/fesm2022/nexterajs-ui.mjs.map +1 -1
- package/package.json +33 -2
- package/styles/_dropdown.scss +5 -5
- package/styles/_editor.scss +52 -17
- package/styles/_mixins.scss +1 -1
- package/styles/_scrollbar.scss +5 -5
- package/styles/_status.scss +21 -0
- package/styles/_toast.scss +14 -14
- package/styles/_tokens.scss +420 -3
- package/styles/_widget.scss +14 -3
- package/styles/index.scss +6 -4
- package/types/nexterajs-ui-charts.d.ts +446 -0
- package/types/nexterajs-ui-core.d.ts +387 -22
- package/types/nexterajs-ui-display.d.ts +720 -0
- package/types/nexterajs-ui-dropdowns.d.ts +191 -48
- package/types/nexterajs-ui-file-uploader.d.ts +236 -7
- package/types/nexterajs-ui-forms.d.ts +675 -30
- package/types/nexterajs-ui-grid.d.ts +448 -93
- package/types/nexterajs-ui-intl.d.ts +39 -4
- package/types/nexterajs-ui-navigation.d.ts +531 -0
- package/types/nexterajs-ui-notify.d.ts +4 -2
- package/types/nexterajs-ui-overlay.d.ts +703 -0
- package/types/nexterajs-ui-pivot.d.ts +56 -48
- package/types/nexterajs-ui-shell.d.ts +294 -0
- package/types/nexterajs-ui-tree-view.d.ts +6 -3
- package/types/nexterajs-ui-validation.d.ts +20 -11
- package/types/nexterajs-ui.d.ts +5 -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, viewChild,
|
|
3
|
-
import { NxeValidationGroupRegistry, NXE_VALIDATION_GROUP, 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'`
|
|
@@ -31,6 +36,10 @@ function cssSize(value) {
|
|
|
31
36
|
* Content is projected (`<nxe-icon> + <span>`, the dominant pattern). The legacy `text`/`type`
|
|
32
37
|
* inputs render a `.nxe-button-text` label for the ~25 attribute-styled buttons.
|
|
33
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.
|
|
42
|
+
*
|
|
34
43
|
* `onClick` emits {@link NxeButtonClickEvent}; native `(click)` also fires (27 sites bind it).
|
|
35
44
|
*/
|
|
36
45
|
class NxeButtonComponent {
|
|
@@ -43,6 +52,19 @@ class NxeButtonComponent {
|
|
|
43
52
|
/** Style intent (`default`=blue, `success`=green, `danger`=red, `normal`=white). */
|
|
44
53
|
type = input('normal', /* @ts-ignore */
|
|
45
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 });
|
|
46
68
|
/** Disabled state (`[disabled]`); blocks `onClick` and dims the button. */
|
|
47
69
|
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
48
70
|
/** Named validation group resolved via {@link NxeValidationGroupRegistry} on click. */
|
|
@@ -75,7 +97,7 @@ class NxeButtonComponent {
|
|
|
75
97
|
this.handleClick(event);
|
|
76
98
|
}
|
|
77
99
|
handleClick(event) {
|
|
78
|
-
if (this.disabled()) {
|
|
100
|
+
if (this.disabled() || this.loading()) {
|
|
79
101
|
event.preventDefault();
|
|
80
102
|
event.stopImmediatePropagation();
|
|
81
103
|
return;
|
|
@@ -92,17 +114,21 @@ class NxeButtonComponent {
|
|
|
92
114
|
});
|
|
93
115
|
}
|
|
94
116
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
95
|
-
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.nxe-button-has-icon": "!!icon()", "style.height": "heightStyle()", "style.width": "widthStyle()", "class.nxe-disabled": "disabled()" }, classAttribute: "nxe-widget" }, ngImport: i0, template: `
|
|
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: `
|
|
96
118
|
<span class="nxe-button-content">
|
|
97
119
|
@if (icon()) {
|
|
98
120
|
<i class="nxe-icon nxe-icon-{{ icon() }}"></i>
|
|
99
121
|
}
|
|
122
|
+
<ng-content select="[nxeButtonIcon], [nxeIcon]" />
|
|
100
123
|
<ng-content />
|
|
101
124
|
@if (text()) {
|
|
102
125
|
<span class="nxe-button-text">{{ text() }}</span>
|
|
103
126
|
}
|
|
104
127
|
</span>
|
|
105
|
-
|
|
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 });
|
|
106
132
|
}
|
|
107
133
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeButtonComponent, decorators: [{
|
|
108
134
|
type: Component,
|
|
@@ -111,16 +137,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
111
137
|
@if (icon()) {
|
|
112
138
|
<i class="nxe-icon nxe-icon-{{ icon() }}"></i>
|
|
113
139
|
}
|
|
140
|
+
<ng-content select="[nxeButtonIcon], [nxeIcon]" />
|
|
114
141
|
<ng-content />
|
|
115
142
|
@if (text()) {
|
|
116
143
|
<span class="nxe-button-text">{{ text() }}</span>
|
|
117
144
|
}
|
|
118
145
|
</span>
|
|
146
|
+
@if (loading()) {
|
|
147
|
+
<span class="nxe-button-spinner" aria-hidden="true"></span>
|
|
148
|
+
}
|
|
119
149
|
`, host: {
|
|
120
150
|
class: 'nxe-widget',
|
|
121
151
|
role: 'button',
|
|
122
152
|
'[attr.tabindex]': 'disabled() || !focusStateEnabled() ? null : 0',
|
|
123
|
-
'[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()',
|
|
124
161
|
'[class.nxe-button-default]': "type() === 'default'",
|
|
125
162
|
'[class.nxe-button-success]': "type() === 'success'",
|
|
126
163
|
'[class.nxe-button-danger]': "type() === 'danger'",
|
|
@@ -132,8 +169,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
132
169
|
'(click)': 'handleClick($event)',
|
|
133
170
|
'(keydown.enter)': 'handleKeydown($event)',
|
|
134
171
|
'(keydown.space)': 'handleKeydown($event)',
|
|
135
|
-
}, 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
|
|
136
|
-
}], 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"] }] } });
|
|
137
174
|
|
|
138
175
|
/**
|
|
139
176
|
* `nxe-text-box` — drop-in replacement for DevExtreme `dx-text-box`.
|
|
@@ -163,6 +200,18 @@ class NxeTextBoxComponent extends NxeFormControlBase {
|
|
|
163
200
|
showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
|
|
164
201
|
showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.textBox?.showClearButton ?? false, /* @ts-ignore */
|
|
165
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 */ []));
|
|
166
215
|
focused = signal(false, /* @ts-ignore */
|
|
167
216
|
...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
|
|
168
217
|
displayValue = computed(() => this.value() ?? '', /* @ts-ignore */
|
|
@@ -200,6 +249,9 @@ class NxeTextBoxComponent extends NxeFormControlBase {
|
|
|
200
249
|
onTypedKeyup(event) {
|
|
201
250
|
this.commitOn('keyup', event.target.value, event);
|
|
202
251
|
}
|
|
252
|
+
commitPending(event) {
|
|
253
|
+
this.commitValue(this.fieldRef().nativeElement.value, event);
|
|
254
|
+
}
|
|
203
255
|
onBlur(event) {
|
|
204
256
|
// Blur commits whatever the mode: a value typed and left must not be discarded because
|
|
205
257
|
// the configured event never fired.
|
|
@@ -208,7 +260,7 @@ class NxeTextBoxComponent extends NxeFormControlBase {
|
|
|
208
260
|
this.markAsTouched();
|
|
209
261
|
}
|
|
210
262
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
211
|
-
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 } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "
|
|
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: [
|
|
212
264
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextBoxComponent), multi: true },
|
|
213
265
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextBoxComponent) },
|
|
214
266
|
], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
|
|
@@ -220,9 +272,9 @@ class NxeTextBoxComponent extends NxeFormControlBase {
|
|
|
220
272
|
[id]="controlId()"
|
|
221
273
|
[attr.aria-label]="ariaLabel() || null"
|
|
222
274
|
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
223
|
-
[attr.aria-invalid]="
|
|
275
|
+
[attr.aria-invalid]="invalidState() ? true : null"
|
|
224
276
|
[attr.aria-required]="required() ? true : null"
|
|
225
|
-
[type]="mode()"
|
|
277
|
+
[type]="passwordRevealed() ? 'text' : mode()"
|
|
226
278
|
[value]="displayValue()"
|
|
227
279
|
[readOnly]="readOnly()"
|
|
228
280
|
[disabled]="disabled()"
|
|
@@ -245,6 +297,24 @@ class NxeTextBoxComponent extends NxeFormControlBase {
|
|
|
245
297
|
</svg>
|
|
246
298
|
</span>
|
|
247
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
|
+
}
|
|
248
318
|
</div>
|
|
249
319
|
<ng-content />
|
|
250
320
|
@if (validationError(); as error) {
|
|
@@ -268,9 +338,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
268
338
|
[id]="controlId()"
|
|
269
339
|
[attr.aria-label]="ariaLabel() || null"
|
|
270
340
|
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
271
|
-
[attr.aria-invalid]="
|
|
341
|
+
[attr.aria-invalid]="invalidState() ? true : null"
|
|
272
342
|
[attr.aria-required]="required() ? true : null"
|
|
273
|
-
[type]="mode()"
|
|
343
|
+
[type]="passwordRevealed() ? 'text' : mode()"
|
|
274
344
|
[value]="displayValue()"
|
|
275
345
|
[readOnly]="readOnly()"
|
|
276
346
|
[disabled]="disabled()"
|
|
@@ -293,6 +363,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
293
363
|
</svg>
|
|
294
364
|
</span>
|
|
295
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
|
+
}
|
|
296
384
|
</div>
|
|
297
385
|
<ng-content />
|
|
298
386
|
@if (validationError(); as error) {
|
|
@@ -304,7 +392,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
304
392
|
host: {
|
|
305
393
|
class: 'nxe-widget nxe-editor nxe-textbox',
|
|
306
394
|
'[class.nxe-focused]': 'focused()',
|
|
307
|
-
'[class.nxe-invalid]': '
|
|
395
|
+
'[class.nxe-invalid]': 'invalidState()',
|
|
308
396
|
},
|
|
309
397
|
providers: [
|
|
310
398
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextBoxComponent), multi: true },
|
|
@@ -313,7 +401,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
313
401
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
314
402
|
encapsulation: ViewEncapsulation.None,
|
|
315
403
|
}]
|
|
316
|
-
}], 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 }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
|
|
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 }] }] } });
|
|
317
405
|
|
|
318
406
|
/**
|
|
319
407
|
* `nxe-text-area` — drop-in replacement for DevExtreme `dx-text-area`.
|
|
@@ -330,17 +418,94 @@ class NxeTextAreaComponent extends NxeFormControlBase {
|
|
|
330
418
|
heightInput = input(undefined, { ...(ngDevMode ? { debugName: "heightInput" } : /* istanbul ignore next */ {}), alias: 'height' });
|
|
331
419
|
height = computed(() => this.heightInput() ?? this.nxeDefaults.textArea?.height ?? 80, /* @ts-ignore */
|
|
332
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)) });
|
|
333
436
|
focused = signal(false, /* @ts-ignore */
|
|
334
437
|
...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
|
|
335
438
|
displayValue = computed(() => this.value() ?? '', /* @ts-ignore */
|
|
336
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
|
+
}
|
|
337
471
|
/** dx `valueChangeEvent: 'input'` — the search-as-you-type case. */
|
|
338
472
|
onTypedInput(event) {
|
|
339
|
-
|
|
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';
|
|
340
495
|
}
|
|
341
496
|
onTypedKeyup(event) {
|
|
342
497
|
this.commitOn('keyup', event.target.value, event);
|
|
343
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
|
+
}
|
|
344
509
|
onBlur(event) {
|
|
345
510
|
// Blur commits whatever the mode: a value typed and left must not be discarded because
|
|
346
511
|
// the configured event never fired.
|
|
@@ -348,75 +513,101 @@ class NxeTextAreaComponent extends NxeFormControlBase {
|
|
|
348
513
|
this.focused.set(false);
|
|
349
514
|
this.markAsTouched();
|
|
350
515
|
}
|
|
351
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, deps:
|
|
352
|
-
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": "
|
|
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: [
|
|
353
518
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextAreaComponent), multi: true },
|
|
354
519
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextAreaComponent) },
|
|
355
|
-
], usesInheritance: true, ngImport: i0, template: `
|
|
356
|
-
<div class="nxe-editor-container">
|
|
357
|
-
<textarea
|
|
358
|
-
class="nxe-editor-input nxe-textarea-input"
|
|
359
|
-
[attr.tabindex]="tabIndex()"
|
|
360
|
-
[id]="controlId()"
|
|
361
|
-
[attr.aria-label]="ariaLabel() || null"
|
|
362
|
-
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
363
|
-
[attr.aria-invalid]="
|
|
364
|
-
[attr.aria-required]="required() ? true : null"
|
|
365
|
-
[value]="displayValue()"
|
|
366
|
-
[readOnly]="readOnly()"
|
|
367
|
-
[disabled]="disabled()"
|
|
368
|
-
[attr.placeholder]="placeholder() || null"
|
|
369
|
-
[
|
|
370
|
-
|
|
371
|
-
(
|
|
372
|
-
(
|
|
373
|
-
(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
@if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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 });
|
|
382
560
|
}
|
|
383
561
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, decorators: [{
|
|
384
562
|
type: Component,
|
|
385
|
-
args: [{ selector: 'nxe-text-area', standalone: true, template: `
|
|
386
|
-
<div class="nxe-editor-container">
|
|
387
|
-
<textarea
|
|
388
|
-
class="nxe-editor-input nxe-textarea-input"
|
|
389
|
-
[attr.tabindex]="tabIndex()"
|
|
390
|
-
[id]="controlId()"
|
|
391
|
-
[attr.aria-label]="ariaLabel() || null"
|
|
392
|
-
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
393
|
-
[attr.aria-invalid]="
|
|
394
|
-
[attr.aria-required]="required() ? true : null"
|
|
395
|
-
[value]="displayValue()"
|
|
396
|
-
[readOnly]="readOnly()"
|
|
397
|
-
[disabled]="disabled()"
|
|
398
|
-
[attr.placeholder]="placeholder() || null"
|
|
399
|
-
[
|
|
400
|
-
|
|
401
|
-
(
|
|
402
|
-
(
|
|
403
|
-
(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
@if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
}
|
|
411
602
|
`, host: {
|
|
412
603
|
class: 'nxe-widget nxe-editor nxe-textarea',
|
|
413
604
|
'[class.nxe-focused]': 'focused()',
|
|
414
|
-
'[class.nxe-invalid]': '
|
|
605
|
+
'[class.nxe-invalid]': 'invalidState()',
|
|
415
606
|
}, providers: [
|
|
416
607
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextAreaComponent), multi: true },
|
|
417
608
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextAreaComponent) },
|
|
418
|
-
], 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"] }]
|
|
419
|
-
}], 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 }] }] } });
|
|
420
611
|
|
|
421
612
|
/**
|
|
422
613
|
* `nxe-check-box` — drop-in replacement for DevExtreme `dx-check-box`.
|
|
@@ -470,36 +661,36 @@ class NxeCheckBoxComponent extends NxeFormControlBase {
|
|
|
470
661
|
this.toggle(event);
|
|
471
662
|
}
|
|
472
663
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeCheckBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
473
|
-
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": "
|
|
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: [
|
|
474
665
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeCheckBoxComponent), multi: true },
|
|
475
666
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeCheckBoxComponent) },
|
|
476
|
-
], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
|
|
477
|
-
<span class="nxe-checkbox-container">
|
|
478
|
-
<span class="nxe-checkbox-icon" aria-hidden="true"></span>
|
|
479
|
-
@if (text()) {
|
|
480
|
-
<span class="nxe-checkbox-text">{{ text() }}</span>
|
|
481
|
-
}
|
|
482
|
-
</span>
|
|
483
|
-
<ng-content />
|
|
484
|
-
`, 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
|
|
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 });
|
|
485
676
|
}
|
|
486
677
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeCheckBoxComponent, decorators: [{
|
|
487
678
|
type: Component,
|
|
488
|
-
args: [{ selector: 'nxe-check-box', standalone: true, template: `
|
|
489
|
-
<span class="nxe-checkbox-container">
|
|
490
|
-
<span class="nxe-checkbox-icon" aria-hidden="true"></span>
|
|
491
|
-
@if (text()) {
|
|
492
|
-
<span class="nxe-checkbox-text">{{ text() }}</span>
|
|
493
|
-
}
|
|
494
|
-
</span>
|
|
495
|
-
<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 />
|
|
496
687
|
`, host: {
|
|
497
688
|
class: 'nxe-widget nxe-checkbox',
|
|
498
689
|
role: 'checkbox',
|
|
499
690
|
'[attr.aria-checked]': "indeterminate() ? 'mixed' : !!value()",
|
|
500
691
|
'[attr.aria-disabled]': 'disabled() ? true : null',
|
|
501
692
|
'[attr.aria-readonly]': 'readOnly() ? true : null',
|
|
502
|
-
'[attr.aria-invalid]': '
|
|
693
|
+
'[attr.aria-invalid]': 'invalidState() ? true : null',
|
|
503
694
|
'[attr.aria-required]': 'required() ? true : null',
|
|
504
695
|
// The host IS the control here, so the id and name belong on it rather than on a child.
|
|
505
696
|
'[attr.id]': 'inputId() ? controlId() : null',
|
|
@@ -511,16 +702,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
511
702
|
'[class.nxe-checkbox-indeterminate]': 'indeterminate()',
|
|
512
703
|
'[class.nxe-disabled]': 'disabled()',
|
|
513
704
|
'[class.nxe-readonly]': 'readOnly()',
|
|
514
|
-
'[class.nxe-invalid]': '
|
|
705
|
+
'[class.nxe-invalid]': 'invalidState()',
|
|
515
706
|
'(click)': 'toggle($event)',
|
|
516
707
|
'(keydown.space)': 'onSpace($event)',
|
|
517
708
|
'(blur)': 'markAsTouched()',
|
|
518
709
|
}, providers: [
|
|
519
710
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeCheckBoxComponent), multi: true },
|
|
520
711
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeCheckBoxComponent) },
|
|
521
|
-
], 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
|
|
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"] }]
|
|
522
713
|
}], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }] } });
|
|
523
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;
|
|
524
767
|
/** Coerce a static (`min="1"`) or bound (`[min]="0"`) attribute to a number, or undefined. */
|
|
525
768
|
function toNumber(value) {
|
|
526
769
|
if (value == null || value === '') {
|
|
@@ -540,6 +783,17 @@ function toNumber(value) {
|
|
|
540
783
|
* Supports `formControlName`/`[(ngModel)]`/`[(value)]`/`[value]`, `[readOnly]`, `[min]`/`[max]`,
|
|
541
784
|
* `format` (string LDML pattern), `(valueChange)` (numeric), and a projected `nxe-validator`.
|
|
542
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
|
+
*
|
|
543
797
|
* `showClearButton` (dx default `false`) adds dx's clear area after the input while the field
|
|
544
798
|
* holds text and is neither read-only nor disabled; it sets the value to `null`, as dx's
|
|
545
799
|
* `clear()` does for a number box, and focuses the input.
|
|
@@ -555,14 +809,61 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
555
809
|
showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
|
|
556
810
|
showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.numberBox?.showClearButton ?? false, /* @ts-ignore */
|
|
557
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 });
|
|
558
825
|
focused = signal(false, /* @ts-ignore */
|
|
559
826
|
...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
|
|
560
|
-
|
|
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 */
|
|
561
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 */ []));
|
|
562
849
|
fieldRef = viewChild.required('field', /* @ts-ignore */
|
|
563
850
|
...(ngDevMode ? [{ debugName: "fieldRef" }] : /* istanbul ignore next */ []));
|
|
564
851
|
/** Locale characters, configured once by the application. Defaults to en-US. */
|
|
565
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 */ []));
|
|
566
867
|
displayValue = computed(() => {
|
|
567
868
|
const value = this.value();
|
|
568
869
|
if (this.focused()) {
|
|
@@ -570,7 +871,7 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
570
871
|
// a JS-style dot, and parseNumber would read that as a group separator and commit 125.
|
|
571
872
|
return value == null ? '' : String(value).replace('.', this.separators.decimal);
|
|
572
873
|
}
|
|
573
|
-
return
|
|
874
|
+
return this.formattedValue();
|
|
574
875
|
}, /* @ts-ignore */
|
|
575
876
|
...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
|
|
576
877
|
/** The text in the field right now; dx shows the clear button by the field's emptiness. */
|
|
@@ -578,6 +879,92 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
578
879
|
...(ngDevMode ? [{ debugName: "fieldText" }] : /* istanbul ignore next */ []));
|
|
579
880
|
clearButtonVisible = computed(() => this.showClearButton() && !this.readOnly() && !this.disabled() && this.fieldText() !== '', /* @ts-ignore */
|
|
580
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
|
+
}
|
|
581
968
|
onFieldInput(event) {
|
|
582
969
|
this.fieldText.set(event.target.value);
|
|
583
970
|
}
|
|
@@ -594,8 +981,16 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
594
981
|
field.focus();
|
|
595
982
|
}
|
|
596
983
|
}
|
|
984
|
+
commitPending(event) {
|
|
985
|
+
this.commitText(this.fieldRef().nativeElement.value, event);
|
|
986
|
+
}
|
|
597
987
|
onBlur(event) {
|
|
598
|
-
|
|
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);
|
|
599
994
|
if (num != null) {
|
|
600
995
|
const min = this.min();
|
|
601
996
|
const max = this.max();
|
|
@@ -611,11 +1006,9 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
611
1006
|
}
|
|
612
1007
|
}
|
|
613
1008
|
this.commitValue(num, event);
|
|
614
|
-
this.focused.set(false);
|
|
615
|
-
this.markAsTouched();
|
|
616
1009
|
}
|
|
617
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, deps:
|
|
618
|
-
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 } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "
|
|
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: [
|
|
619
1012
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeNumberBoxComponent), multi: true },
|
|
620
1013
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeNumberBoxComponent) },
|
|
621
1014
|
], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
|
|
@@ -627,15 +1020,21 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
627
1020
|
[id]="controlId()"
|
|
628
1021
|
[attr.aria-label]="ariaLabel() || null"
|
|
629
1022
|
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
630
|
-
[attr.aria-invalid]="
|
|
1023
|
+
[attr.aria-invalid]="invalidState() ? true : null"
|
|
631
1024
|
[attr.aria-required]="required() ? true : null"
|
|
632
1025
|
type="text"
|
|
633
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()"
|
|
634
1032
|
[value]="displayValue()"
|
|
635
1033
|
[readOnly]="readOnly()"
|
|
636
1034
|
[disabled]="disabled()"
|
|
637
1035
|
autocomplete="off"
|
|
638
1036
|
(input)="onFieldInput($event)"
|
|
1037
|
+
(keydown)="onKeyDownStep($event)"
|
|
639
1038
|
(focus)="focused.set(true)"
|
|
640
1039
|
(blur)="onBlur($event)" />
|
|
641
1040
|
@if (clearButtonVisible()) {
|
|
@@ -651,6 +1050,44 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
651
1050
|
</svg>
|
|
652
1051
|
</span>
|
|
653
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
|
+
}
|
|
654
1091
|
</div>
|
|
655
1092
|
<ng-content />
|
|
656
1093
|
@if (validationError(); as error) {
|
|
@@ -658,7 +1095,7 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
|
|
|
658
1095
|
<div class="nxe-invalid-message">{{ error }}</div>
|
|
659
1096
|
}
|
|
660
1097
|
}
|
|
661
|
-
`, isInline: true, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
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 });
|
|
662
1099
|
}
|
|
663
1100
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, decorators: [{
|
|
664
1101
|
type: Component,
|
|
@@ -671,15 +1108,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
671
1108
|
[id]="controlId()"
|
|
672
1109
|
[attr.aria-label]="ariaLabel() || null"
|
|
673
1110
|
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
674
|
-
[attr.aria-invalid]="
|
|
1111
|
+
[attr.aria-invalid]="invalidState() ? true : null"
|
|
675
1112
|
[attr.aria-required]="required() ? true : null"
|
|
676
1113
|
type="text"
|
|
677
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()"
|
|
678
1120
|
[value]="displayValue()"
|
|
679
1121
|
[readOnly]="readOnly()"
|
|
680
1122
|
[disabled]="disabled()"
|
|
681
1123
|
autocomplete="off"
|
|
682
1124
|
(input)="onFieldInput($event)"
|
|
1125
|
+
(keydown)="onKeyDownStep($event)"
|
|
683
1126
|
(focus)="focused.set(true)"
|
|
684
1127
|
(blur)="onBlur($event)" />
|
|
685
1128
|
@if (clearButtonVisible()) {
|
|
@@ -695,6 +1138,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
695
1138
|
</svg>
|
|
696
1139
|
</span>
|
|
697
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
|
+
}
|
|
698
1179
|
</div>
|
|
699
1180
|
<ng-content />
|
|
700
1181
|
@if (validationError(); as error) {
|
|
@@ -705,12 +1186,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
705
1186
|
`, host: {
|
|
706
1187
|
class: 'nxe-widget nxe-editor nxe-numberbox',
|
|
707
1188
|
'[class.nxe-focused]': 'focused()',
|
|
708
|
-
'[class.nxe-invalid]': '
|
|
1189
|
+
'[class.nxe-invalid]': 'invalidState()',
|
|
709
1190
|
}, providers: [
|
|
710
1191
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeNumberBoxComponent), multi: true },
|
|
711
1192
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeNumberBoxComponent) },
|
|
712
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}\n"] }]
|
|
713
|
-
}], 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 }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
|
|
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 }] }] } });
|
|
714
1195
|
|
|
715
1196
|
/** Normalize a `[min]`/`[max]` binding (Date object or serialized string) to a DateTime. */
|
|
716
1197
|
function toBoundDate(bound) {
|
|
@@ -774,7 +1255,12 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
774
1255
|
/** Calendar zoom level; caption clicks zoom out, cell picks zoom back in (dx-calendar parity). */
|
|
775
1256
|
zoomLevel = signal('month', /* @ts-ignore */
|
|
776
1257
|
...(ngDevMode ? [{ debugName: "zoomLevel" }] : /* istanbul ignore next */ []));
|
|
777
|
-
|
|
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 */ []));
|
|
778
1264
|
/** The format actually used, from `displayFormat` or the default for this `type`. */
|
|
779
1265
|
effectiveDisplayFormat = computed(() => {
|
|
780
1266
|
const stated = this.displayFormat() ?? this.nxeDefaults.dateBox?.displayFormat;
|
|
@@ -794,7 +1280,8 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
794
1280
|
/** dx's default placeholder is '', but orbit's global default is the format itself. */
|
|
795
1281
|
effectivePlaceholder = computed(() => this.placeholder() ?? this.nxeDefaults.dateBox?.placeholder ?? this.effectiveDisplayFormat(), /* @ts-ignore */
|
|
796
1282
|
...(ngDevMode ? [{ debugName: "effectivePlaceholder" }] : /* istanbul ignore next */ []));
|
|
797
|
-
|
|
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 */
|
|
798
1285
|
...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
799
1286
|
displayValue = computed(() => formatDisplay(this.selected(), this.effectiveDisplayFormat()), /* @ts-ignore */
|
|
800
1287
|
...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
|
|
@@ -816,7 +1303,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
816
1303
|
*/
|
|
817
1304
|
textAtFocus = '';
|
|
818
1305
|
caption = computed(() => {
|
|
819
|
-
const view = this.viewMonth();
|
|
1306
|
+
const view = this.viewMonth().setLocale(this.nxeMessages.locale());
|
|
820
1307
|
switch (this.zoomLevel()) {
|
|
821
1308
|
case 'month':
|
|
822
1309
|
return view.toFormat('MMMM yyyy');
|
|
@@ -842,7 +1329,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
842
1329
|
const min = this.minDate();
|
|
843
1330
|
const max = this.maxDate();
|
|
844
1331
|
const span = zoom === 'year' ? { months: 1 } : zoom === 'decade' ? { years: 1 } : { years: 10 };
|
|
845
|
-
return buildPeriodCells(zoom, this.viewMonth()).map((cell) => {
|
|
1332
|
+
return buildPeriodCells(zoom, this.viewMonth().setLocale(this.nxeMessages.locale())).map((cell) => {
|
|
846
1333
|
const end = cell.anchor.plus(span).minus({ days: 1 }).endOf('day');
|
|
847
1334
|
return {
|
|
848
1335
|
...cell,
|
|
@@ -953,6 +1440,15 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
953
1440
|
const hadBaseline = this.focused();
|
|
954
1441
|
this.focused.set(false);
|
|
955
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) {
|
|
956
1452
|
if (hadBaseline && element.value === this.textAtFocus) {
|
|
957
1453
|
return;
|
|
958
1454
|
}
|
|
@@ -961,7 +1457,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
961
1457
|
this.syncField();
|
|
962
1458
|
return;
|
|
963
1459
|
}
|
|
964
|
-
const dt = parseDisplay(element.value, this.effectiveDisplayFormat(), this.selected());
|
|
1460
|
+
const dt = parseDisplay(element.value, this.effectiveDisplayFormat(), this.selected(), this.nxeMessages.locale());
|
|
965
1461
|
if (dt) {
|
|
966
1462
|
this.commitValue(serializeDate(this.clampToBounds(dt)), event);
|
|
967
1463
|
}
|
|
@@ -1066,7 +1562,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
1066
1562
|
this.overlayRef?.dispose();
|
|
1067
1563
|
}
|
|
1068
1564
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1069
|
-
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": "
|
|
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: [
|
|
1070
1566
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateBoxComponent), multi: true },
|
|
1071
1567
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateBoxComponent) },
|
|
1072
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: `
|
|
@@ -1078,7 +1574,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
1078
1574
|
[id]="controlId()"
|
|
1079
1575
|
[attr.aria-label]="ariaLabel() || null"
|
|
1080
1576
|
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
1081
|
-
[attr.aria-invalid]="
|
|
1577
|
+
[attr.aria-invalid]="invalidState() ? true : null"
|
|
1082
1578
|
[attr.aria-required]="required() ? true : null"
|
|
1083
1579
|
type="text"
|
|
1084
1580
|
[value]="displayValue()"
|
|
@@ -1180,7 +1676,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
1180
1676
|
</div>
|
|
1181
1677
|
@if (zoomLevel() === 'month') {
|
|
1182
1678
|
<div class="nxe-calendar-weekdays">
|
|
1183
|
-
@for (wd of weekdays; track
|
|
1679
|
+
@for (wd of weekdays(); track $index) {
|
|
1184
1680
|
<span>{{ wd }}</span>
|
|
1185
1681
|
}
|
|
1186
1682
|
</div>
|
|
@@ -1225,7 +1721,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
1225
1721
|
type="text"
|
|
1226
1722
|
inputmode="numeric"
|
|
1227
1723
|
maxlength="2"
|
|
1228
|
-
aria-label="
|
|
1724
|
+
[attr.aria-label]="nxeMessages.t('date.hour')"
|
|
1229
1725
|
[value]="hourText()"
|
|
1230
1726
|
(change)="setTimePart('hour', $event)" />
|
|
1231
1727
|
<span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
|
|
@@ -1234,18 +1730,18 @@ class NxeDateBoxComponent extends NxeFormControlBase {
|
|
|
1234
1730
|
type="text"
|
|
1235
1731
|
inputmode="numeric"
|
|
1236
1732
|
maxlength="2"
|
|
1237
|
-
aria-label="
|
|
1733
|
+
[attr.aria-label]="nxeMessages.t('date.minute')"
|
|
1238
1734
|
[value]="minuteText()"
|
|
1239
1735
|
(change)="setTimePart('minute', $event)" />
|
|
1240
|
-
<button type="button" class="nxe-calendar-time-btn" (click)="setNow()">
|
|
1736
|
+
<button type="button" class="nxe-calendar-time-btn" (click)="setNow()">{{ nxeMessages.t('date.now') }}</button>
|
|
1241
1737
|
<button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
|
|
1242
|
-
|
|
1738
|
+
{{ nxeMessages.t('date.done') }}
|
|
1243
1739
|
</button>
|
|
1244
1740
|
</div>
|
|
1245
1741
|
}
|
|
1246
1742
|
</div>
|
|
1247
1743
|
</ng-template>
|
|
1248
|
-
`, 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
|
|
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 });
|
|
1249
1745
|
}
|
|
1250
1746
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateBoxComponent, decorators: [{
|
|
1251
1747
|
type: Component,
|
|
@@ -1258,7 +1754,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
1258
1754
|
[id]="controlId()"
|
|
1259
1755
|
[attr.aria-label]="ariaLabel() || null"
|
|
1260
1756
|
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
1261
|
-
[attr.aria-invalid]="
|
|
1757
|
+
[attr.aria-invalid]="invalidState() ? true : null"
|
|
1262
1758
|
[attr.aria-required]="required() ? true : null"
|
|
1263
1759
|
type="text"
|
|
1264
1760
|
[value]="displayValue()"
|
|
@@ -1360,7 +1856,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
1360
1856
|
</div>
|
|
1361
1857
|
@if (zoomLevel() === 'month') {
|
|
1362
1858
|
<div class="nxe-calendar-weekdays">
|
|
1363
|
-
@for (wd of weekdays; track
|
|
1859
|
+
@for (wd of weekdays(); track $index) {
|
|
1364
1860
|
<span>{{ wd }}</span>
|
|
1365
1861
|
}
|
|
1366
1862
|
</div>
|
|
@@ -1405,7 +1901,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
1405
1901
|
type="text"
|
|
1406
1902
|
inputmode="numeric"
|
|
1407
1903
|
maxlength="2"
|
|
1408
|
-
aria-label="
|
|
1904
|
+
[attr.aria-label]="nxeMessages.t('date.hour')"
|
|
1409
1905
|
[value]="hourText()"
|
|
1410
1906
|
(change)="setTimePart('hour', $event)" />
|
|
1411
1907
|
<span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
|
|
@@ -1414,12 +1910,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
1414
1910
|
type="text"
|
|
1415
1911
|
inputmode="numeric"
|
|
1416
1912
|
maxlength="2"
|
|
1417
|
-
aria-label="
|
|
1913
|
+
[attr.aria-label]="nxeMessages.t('date.minute')"
|
|
1418
1914
|
[value]="minuteText()"
|
|
1419
1915
|
(change)="setTimePart('minute', $event)" />
|
|
1420
|
-
<button type="button" class="nxe-calendar-time-btn" (click)="setNow()">
|
|
1916
|
+
<button type="button" class="nxe-calendar-time-btn" (click)="setNow()">{{ nxeMessages.t('date.now') }}</button>
|
|
1421
1917
|
<button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
|
|
1422
|
-
|
|
1918
|
+
{{ nxeMessages.t('date.done') }}
|
|
1423
1919
|
</button>
|
|
1424
1920
|
</div>
|
|
1425
1921
|
}
|
|
@@ -1428,13 +1924,634 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
1428
1924
|
`, host: {
|
|
1429
1925
|
class: 'nxe-widget nxe-editor nxe-datebox',
|
|
1430
1926
|
'[class.nxe-focused]': 'focused()',
|
|
1431
|
-
'[class.nxe-invalid]': '
|
|
1927
|
+
'[class.nxe-invalid]': 'invalidState()',
|
|
1432
1928
|
}, providers: [
|
|
1433
1929
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateBoxComponent), multi: true },
|
|
1434
1930
|
{ provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateBoxComponent) },
|
|
1435
|
-
], 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
|
|
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"] }]
|
|
1436
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 }] }] } });
|
|
1437
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\">–</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\">–</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 }] }] } });
|
|
2554
|
+
|
|
1438
2555
|
/**
|
|
1439
2556
|
* `nxe-scroll-view` — drop-in replacement for DevExtreme `dx-scroll-view`.
|
|
1440
2557
|
*
|
|
@@ -1455,18 +2572,792 @@ class NxeScrollViewComponent {
|
|
|
1455
2572
|
showScrollbar = input('onScroll', /* @ts-ignore */
|
|
1456
2573
|
...(ngDevMode ? [{ debugName: "showScrollbar" }] : /* istanbul ignore next */ []));
|
|
1457
2574
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1458
|
-
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
|
|
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 });
|
|
1459
2576
|
}
|
|
1460
2577
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollViewComponent, decorators: [{
|
|
1461
2578
|
type: Component,
|
|
1462
|
-
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
|
|
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"] }]
|
|
1463
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 }] }] } });
|
|
1464
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
|
+
|
|
1465
3356
|
// @nexterajs/ui/forms — public API.
|
|
1466
3357
|
|
|
1467
3358
|
/**
|
|
1468
3359
|
* Generated bundle index. Do not edit.
|
|
1469
3360
|
*/
|
|
1470
3361
|
|
|
1471
|
-
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 };
|
|
1472
3363
|
//# sourceMappingURL=nexterajs-ui-forms.mjs.map
|