@nexterajs/ui 0.0.1
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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/README.md +64 -0
- package/fesm2022/nexterajs-ui-animations.mjs +184 -0
- package/fesm2022/nexterajs-ui-animations.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-core.mjs +326 -0
- package/fesm2022/nexterajs-ui-core.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs +953 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-excel.mjs +779 -0
- package/fesm2022/nexterajs-ui-excel.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-file-uploader.mjs +125 -0
- package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-forms.mjs +1113 -0
- package/fesm2022/nexterajs-ui-forms.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-grid.mjs +4145 -0
- package/fesm2022/nexterajs-ui-grid.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-intl.mjs +377 -0
- package/fesm2022/nexterajs-ui-intl.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-pivot.mjs +4739 -0
- package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-styling.mjs +59 -0
- package/fesm2022/nexterajs-ui-styling.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs +202 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-validation.mjs +269 -0
- package/fesm2022/nexterajs-ui-validation.mjs.map +1 -0
- package/fesm2022/nexterajs-ui.mjs +23 -0
- package/fesm2022/nexterajs-ui.mjs.map +1 -0
- package/package.json +100 -0
- package/styles/_button-directives.scss +39 -0
- package/styles/_dropdown.scss +113 -0
- package/styles/_editor.scss +146 -0
- package/styles/_mixins.scss +2 -0
- package/styles/_tokens.scss +77 -0
- package/styles/_widget.scss +58 -0
- package/styles/index.scss +83 -0
- package/types/nexterajs-ui-animations.d.ts +22 -0
- package/types/nexterajs-ui-core.d.ts +328 -0
- package/types/nexterajs-ui-dropdowns.d.ts +334 -0
- package/types/nexterajs-ui-excel.d.ts +159 -0
- package/types/nexterajs-ui-file-uploader.d.ts +47 -0
- package/types/nexterajs-ui-forms.d.ts +296 -0
- package/types/nexterajs-ui-grid.d.ts +1485 -0
- package/types/nexterajs-ui-intl.d.ts +125 -0
- package/types/nexterajs-ui-pivot.d.ts +1470 -0
- package/types/nexterajs-ui-styling.d.ts +40 -0
- package/types/nexterajs-ui-tree-view.d.ts +69 -0
- package/types/nexterajs-ui-validation.d.ts +102 -0
- package/types/nexterajs-ui.d.ts +12 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Marker class carried by every nxe surface — component hosts and CDK overlay panes alike.
|
|
5
|
+
*
|
|
6
|
+
* It is the nxe counterpart of DevExtreme's `.dx-widget` base and is what gives a surface its
|
|
7
|
+
* font family/size/weight/line-height (see styling/_widget.scss).
|
|
8
|
+
*
|
|
9
|
+
* It matters most on overlays: a CDK pane is appended to `<body>`, so a portaled dropdown
|
|
10
|
+
* inherits the page, not the editor that opened it. Without this class a panel silently falls
|
|
11
|
+
* back to the host app's body font — which is how the select-box list ended up rendering 14px in
|
|
12
|
+
* an app whose every other text surface is 12.24px.
|
|
13
|
+
*/
|
|
14
|
+
const NXE_WIDGET_CLASS = 'nxe-widget';
|
|
15
|
+
/** Set on the pane while the overlay is rendered ABOVE its origin instead of below. */
|
|
16
|
+
const NXE_PANE_FLIPPED_CLASS = 'nxe-pane-flipped';
|
|
17
|
+
/**
|
|
18
|
+
* Build the `panelClass` for a library-created overlay, guaranteeing the widget contract.
|
|
19
|
+
*
|
|
20
|
+
* panelClass: nxePane('nxe-select-pane')
|
|
21
|
+
* panelClass: nxePane('nxe-dropdownbox-pane', ...wrapperClass.split(/\s+/))
|
|
22
|
+
*
|
|
23
|
+
* Blank/undefined entries are dropped so callers can splat user-supplied class strings directly.
|
|
24
|
+
*/
|
|
25
|
+
function nxePane(...extra) {
|
|
26
|
+
return [NXE_WIDGET_CLASS, ...extra.filter((c) => !!c && c.trim().length > 0)];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Track whether a connected overlay has flipped above its origin, exposing it as
|
|
30
|
+
* {@link NXE_PANE_FLIPPED_CLASS} on the pane.
|
|
31
|
+
*
|
|
32
|
+
* DevExtreme drew dropdowns flush against the editor — no border on the shared edge — and moved
|
|
33
|
+
* that seam to the other side when the popup flipped up
|
|
34
|
+
* (`.dx-overlay-content.dx-dropdowneditor-overlay-flipped { border-top-width:1px; border-bottom-width:0 }`).
|
|
35
|
+
* CDK does not emit any equivalent hook, so panels need this to reproduce it.
|
|
36
|
+
*
|
|
37
|
+
* Returns the subscription; callers dispose it with the overlay.
|
|
38
|
+
*/
|
|
39
|
+
function trackPaneFlip(overlayRef) {
|
|
40
|
+
const strategy = overlayRef.getConfig().positionStrategy;
|
|
41
|
+
if (!(strategy instanceof FlexibleConnectedPositionStrategy)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return strategy.positionChanges.subscribe((change) => {
|
|
45
|
+
// Flipped == the overlay's TOP edge is anchored to the origin's top, i.e. it grew upward.
|
|
46
|
+
const flipped = change.connectionPair.overlayY === 'bottom';
|
|
47
|
+
const pane = overlayRef.overlayElement;
|
|
48
|
+
pane?.classList.toggle(NXE_PANE_FLIPPED_CLASS, flipped);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// @nexterajs/ui/styling — public API.
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generated bundle index. Do not edit.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
export { NXE_PANE_FLIPPED_CLASS, NXE_WIDGET_CLASS, nxePane, trackPaneFlip };
|
|
59
|
+
//# sourceMappingURL=nexterajs-ui-styling.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nexterajs-ui-styling.mjs","sources":["../../../projects/nextera-ui/styling/src/lib/styling/nxe-overlay.ts","../../../projects/nextera-ui/styling/src/public-api.ts","../../../projects/nextera-ui/styling/src/nexterajs-ui-styling.ts"],"sourcesContent":["import { ConnectedOverlayPositionChange, FlexibleConnectedPositionStrategy, OverlayRef } from '@angular/cdk/overlay';\r\nimport { Subscription } from 'rxjs';\r\n\r\n/**\r\n * Marker class carried by every nxe surface — component hosts and CDK overlay panes alike.\r\n *\r\n * It is the nxe counterpart of DevExtreme's `.dx-widget` base and is what gives a surface its\r\n * font family/size/weight/line-height (see styling/_widget.scss).\r\n *\r\n * It matters most on overlays: a CDK pane is appended to `<body>`, so a portaled dropdown\r\n * inherits the page, not the editor that opened it. Without this class a panel silently falls\r\n * back to the host app's body font — which is how the select-box list ended up rendering 14px in\r\n * an app whose every other text surface is 12.24px.\r\n */\r\nexport const NXE_WIDGET_CLASS = 'nxe-widget';\r\n\r\n/** Set on the pane while the overlay is rendered ABOVE its origin instead of below. */\r\nexport const NXE_PANE_FLIPPED_CLASS = 'nxe-pane-flipped';\r\n\r\n/**\r\n * Build the `panelClass` for a library-created overlay, guaranteeing the widget contract.\r\n *\r\n * panelClass: nxePane('nxe-select-pane')\r\n * panelClass: nxePane('nxe-dropdownbox-pane', ...wrapperClass.split(/\\s+/))\r\n *\r\n * Blank/undefined entries are dropped so callers can splat user-supplied class strings directly.\r\n */\r\nexport function nxePane(...extra: (string | null | undefined)[]): string[] {\r\n return [NXE_WIDGET_CLASS, ...extra.filter((c): c is string => !!c && c.trim().length > 0)];\r\n}\r\n\r\n/**\r\n * Track whether a connected overlay has flipped above its origin, exposing it as\r\n * {@link NXE_PANE_FLIPPED_CLASS} on the pane.\r\n *\r\n * DevExtreme drew dropdowns flush against the editor — no border on the shared edge — and moved\r\n * that seam to the other side when the popup flipped up\r\n * (`.dx-overlay-content.dx-dropdowneditor-overlay-flipped { border-top-width:1px; border-bottom-width:0 }`).\r\n * CDK does not emit any equivalent hook, so panels need this to reproduce it.\r\n *\r\n * Returns the subscription; callers dispose it with the overlay.\r\n */\r\nexport function trackPaneFlip(overlayRef: OverlayRef): Subscription | undefined {\r\n const strategy = overlayRef.getConfig().positionStrategy;\r\n if (!(strategy instanceof FlexibleConnectedPositionStrategy)) {\r\n return undefined;\r\n }\r\n return strategy.positionChanges.subscribe((change: ConnectedOverlayPositionChange) => {\r\n // Flipped == the overlay's TOP edge is anchored to the origin's top, i.e. it grew upward.\r\n const flipped = change.connectionPair.overlayY === 'bottom';\r\n const pane = overlayRef.overlayElement;\r\n pane?.classList.toggle(NXE_PANE_FLIPPED_CLASS, flipped);\r\n });\r\n}\r\n","// @nexterajs/ui/styling — public API.\r\nexport * from './lib/styling/nxe-overlay';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAGA;;;;;;;;;;AAUG;AACI,MAAM,gBAAgB,GAAG;AAEhC;AACO,MAAM,sBAAsB,GAAG;AAEtC;;;;;;;AAOG;AACG,SAAU,OAAO,CAAC,GAAG,KAAoC,EAAA;IAC3D,OAAO,CAAC,gBAAgB,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC9F;AAEA;;;;;;;;;;AAUG;AACG,SAAU,aAAa,CAAC,UAAsB,EAAA;IAChD,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC,gBAAgB;AACxD,IAAA,IAAI,EAAE,QAAQ,YAAY,iCAAiC,CAAC,EAAE;AAC1D,QAAA,OAAO,SAAS;IACpB;IACA,OAAO,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,MAAsC,KAAI;;QAEjF,MAAM,OAAO,GAAG,MAAM,CAAC,cAAc,CAAC,QAAQ,KAAK,QAAQ;AAC3D,QAAA,MAAM,IAAI,GAAG,UAAU,CAAC,cAAc;QACtC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,sBAAsB,EAAE,OAAO,CAAC;AAC3D,IAAA,CAAC,CAAC;AACN;;ACrDA;;ACAA;;AAEG;;"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, output, signal, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `nxe-tree-view` — drop-in replacement for DevExtreme `dx-tree-view`.
|
|
6
|
+
*
|
|
7
|
+
* The single usage (settings sidebar) feeds a FLAT list (`dataStructure="plain"`) and lets the
|
|
8
|
+
* widget assemble the hierarchy via `keyExpr`/`parentIdExpr`/`rootValue`; so this builds the tree
|
|
9
|
+
* internally. Single selection, `selectByClick`, optional search box, and a programmatic
|
|
10
|
+
* `expandAll()` (called via `@ViewChild(NxeTreeViewComponent).instance.expandAll()` after async
|
|
11
|
+
* load). `(onItemClick)` emits `{ itemData: <full original row>, ... }` so the navigation handler
|
|
12
|
+
* keeps reading `event.itemData.url/urlParams/objectName`.
|
|
13
|
+
*/
|
|
14
|
+
class NxeTreeViewComponent {
|
|
15
|
+
dataSource = input([], /* @ts-ignore */
|
|
16
|
+
...(ngDevMode ? [{ debugName: "dataSource" }] : /* istanbul ignore next */ []));
|
|
17
|
+
/** Accepted for parity; this component always assembles a tree from the flat list. */
|
|
18
|
+
dataStructure = input('plain', /* @ts-ignore */
|
|
19
|
+
...(ngDevMode ? [{ debugName: "dataStructure" }] : /* istanbul ignore next */ []));
|
|
20
|
+
keyExpr = input('id', /* @ts-ignore */
|
|
21
|
+
...(ngDevMode ? [{ debugName: "keyExpr" }] : /* istanbul ignore next */ []));
|
|
22
|
+
displayExpr = input('text', /* @ts-ignore */
|
|
23
|
+
...(ngDevMode ? [{ debugName: "displayExpr" }] : /* istanbul ignore next */ []));
|
|
24
|
+
parentIdExpr = input('parentId', /* @ts-ignore */
|
|
25
|
+
...(ngDevMode ? [{ debugName: "parentIdExpr" }] : /* istanbul ignore next */ []));
|
|
26
|
+
rootValue = input(0, /* @ts-ignore */
|
|
27
|
+
...(ngDevMode ? [{ debugName: "rootValue" }] : /* istanbul ignore next */ []));
|
|
28
|
+
/** Accepted for parity (no-op: rows carry no `expanded` field; use `expandAll()`). */
|
|
29
|
+
expandedExpr = input('expanded', /* @ts-ignore */
|
|
30
|
+
...(ngDevMode ? [{ debugName: "expandedExpr" }] : /* istanbul ignore next */ []));
|
|
31
|
+
selectionMode = input('single', /* @ts-ignore */
|
|
32
|
+
...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
|
|
33
|
+
selectByClick = input(false, /* @ts-ignore */
|
|
34
|
+
...(ngDevMode ? [{ debugName: "selectByClick" }] : /* istanbul ignore next */ []));
|
|
35
|
+
searchEnabled = input(false, /* @ts-ignore */
|
|
36
|
+
...(ngDevMode ? [{ debugName: "searchEnabled" }] : /* istanbul ignore next */ []));
|
|
37
|
+
searchMode = input('contains', /* @ts-ignore */
|
|
38
|
+
...(ngDevMode ? [{ debugName: "searchMode" }] : /* istanbul ignore next */ []));
|
|
39
|
+
/** Accepted for parity (native scrolling is the default here anyway). */
|
|
40
|
+
useNativeScrolling = input(true, /* @ts-ignore */
|
|
41
|
+
...(ngDevMode ? [{ debugName: "useNativeScrolling" }] : /* istanbul ignore next */ []));
|
|
42
|
+
width = input(undefined, /* @ts-ignore */
|
|
43
|
+
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
44
|
+
onItemClick = output();
|
|
45
|
+
expanded = signal(new Set(), /* @ts-ignore */
|
|
46
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
47
|
+
selectedKey = signal(null, /* @ts-ignore */
|
|
48
|
+
...(ngDevMode ? [{ debugName: "selectedKey" }] : /* istanbul ignore next */ []));
|
|
49
|
+
searchText = signal('', /* @ts-ignore */
|
|
50
|
+
...(ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []));
|
|
51
|
+
/** DevExtreme `.instance` facade — `expandAll()` is the only method used. */
|
|
52
|
+
get instance() {
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
widthStyle = computed(() => {
|
|
56
|
+
const w = this.width();
|
|
57
|
+
return typeof w === 'number' ? `${w}px` : w;
|
|
58
|
+
}, /* @ts-ignore */
|
|
59
|
+
...(ngDevMode ? [{ debugName: "widthStyle" }] : /* istanbul ignore next */ []));
|
|
60
|
+
tree = computed(() => {
|
|
61
|
+
const rows = (this.dataSource() ?? []);
|
|
62
|
+
const keyF = this.keyExpr();
|
|
63
|
+
const parentF = this.parentIdExpr();
|
|
64
|
+
const dispF = this.displayExpr();
|
|
65
|
+
const root = this.rootValue();
|
|
66
|
+
const byKey = new Map();
|
|
67
|
+
for (const r of rows) {
|
|
68
|
+
byKey.set(r[keyF], {
|
|
69
|
+
key: r[keyF],
|
|
70
|
+
text: String(r[dispF] ?? ''),
|
|
71
|
+
data: r,
|
|
72
|
+
children: [],
|
|
73
|
+
hasChildren: false,
|
|
74
|
+
level: 0,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const roots = [];
|
|
78
|
+
for (const r of rows) {
|
|
79
|
+
const node = byKey.get(r[keyF]);
|
|
80
|
+
const parentKey = r[parentF];
|
|
81
|
+
const parent = parentKey !== root ? byKey.get(parentKey) : undefined;
|
|
82
|
+
if (parent) {
|
|
83
|
+
parent.children.push(node);
|
|
84
|
+
parent.hasChildren = true;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
roots.push(node);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const assignLevels = (nodes, level) => {
|
|
91
|
+
for (const n of nodes) {
|
|
92
|
+
n.level = level;
|
|
93
|
+
assignLevels(n.children, level + 1);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
assignLevels(roots, 0);
|
|
97
|
+
return roots;
|
|
98
|
+
}, /* @ts-ignore */
|
|
99
|
+
...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
|
|
100
|
+
/** Flattened list of currently-visible nodes (respects expand state + search). */
|
|
101
|
+
visibleNodes = computed(() => {
|
|
102
|
+
const out = [];
|
|
103
|
+
const query = this.searchText().trim().toLowerCase();
|
|
104
|
+
if (query) {
|
|
105
|
+
const match = (text) => {
|
|
106
|
+
const t = text.toLowerCase();
|
|
107
|
+
switch (this.searchMode()) {
|
|
108
|
+
case 'startswith':
|
|
109
|
+
return t.startsWith(query);
|
|
110
|
+
case 'equals':
|
|
111
|
+
return t === query;
|
|
112
|
+
default:
|
|
113
|
+
return t.includes(query);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const walkAll = (nodes) => {
|
|
117
|
+
for (const n of nodes) {
|
|
118
|
+
if (match(n.text)) {
|
|
119
|
+
out.push(n);
|
|
120
|
+
}
|
|
121
|
+
walkAll(n.children);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
walkAll(this.tree());
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const expanded = this.expanded();
|
|
128
|
+
const walk = (nodes) => {
|
|
129
|
+
for (const n of nodes) {
|
|
130
|
+
out.push(n);
|
|
131
|
+
if (n.hasChildren && expanded.has(n.key)) {
|
|
132
|
+
walk(n.children);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
walk(this.tree());
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}, /* @ts-ignore */
|
|
140
|
+
...(ngDevMode ? [{ debugName: "visibleNodes" }] : /* istanbul ignore next */ []));
|
|
141
|
+
expandAll() {
|
|
142
|
+
const keys = new Set();
|
|
143
|
+
const walk = (nodes) => {
|
|
144
|
+
for (const n of nodes) {
|
|
145
|
+
if (n.hasChildren) {
|
|
146
|
+
keys.add(n.key);
|
|
147
|
+
}
|
|
148
|
+
walk(n.children);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
walk(this.tree());
|
|
152
|
+
this.expanded.set(keys);
|
|
153
|
+
}
|
|
154
|
+
collapseAll() {
|
|
155
|
+
this.expanded.set(new Set());
|
|
156
|
+
}
|
|
157
|
+
isExpanded(node) {
|
|
158
|
+
return this.expanded().has(node.key);
|
|
159
|
+
}
|
|
160
|
+
toggle(node, event) {
|
|
161
|
+
event.stopPropagation();
|
|
162
|
+
this.expanded.update((set) => {
|
|
163
|
+
const next = new Set(set);
|
|
164
|
+
next.has(node.key) ? next.delete(node.key) : next.add(node.key);
|
|
165
|
+
return next;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
onNodeClick(node, event) {
|
|
169
|
+
if (this.selectByClick() && this.selectionMode() !== 'none') {
|
|
170
|
+
this.selectedKey.set(node.key);
|
|
171
|
+
}
|
|
172
|
+
this.onItemClick.emit({
|
|
173
|
+
itemData: node.data,
|
|
174
|
+
node,
|
|
175
|
+
event,
|
|
176
|
+
component: this,
|
|
177
|
+
element: event.currentTarget,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
onSearch(event) {
|
|
181
|
+
this.searchText.set(event.target.value);
|
|
182
|
+
}
|
|
183
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTreeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
184
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTreeViewComponent, isStandalone: true, selector: "nxe-tree-view", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, dataStructure: { classPropertyName: "dataStructure", publicName: "dataStructure", isSignal: true, isRequired: false, transformFunction: null }, keyExpr: { classPropertyName: "keyExpr", publicName: "keyExpr", isSignal: true, isRequired: false, transformFunction: null }, displayExpr: { classPropertyName: "displayExpr", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, parentIdExpr: { classPropertyName: "parentIdExpr", publicName: "parentIdExpr", isSignal: true, isRequired: false, transformFunction: null }, rootValue: { classPropertyName: "rootValue", publicName: "rootValue", isSignal: true, isRequired: false, transformFunction: null }, expandedExpr: { classPropertyName: "expandedExpr", publicName: "expandedExpr", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, selectByClick: { classPropertyName: "selectByClick", publicName: "selectByClick", isSignal: true, isRequired: false, transformFunction: null }, searchEnabled: { classPropertyName: "searchEnabled", publicName: "searchEnabled", isSignal: true, isRequired: false, transformFunction: null }, searchMode: { classPropertyName: "searchMode", publicName: "searchMode", isSignal: true, isRequired: false, transformFunction: null }, useNativeScrolling: { classPropertyName: "useNativeScrolling", publicName: "useNativeScrolling", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onItemClick: "onItemClick" }, host: { properties: { "class.nxe-treeview-with-search": "searchEnabled()", "style.width": "widthStyle()" }, classAttribute: "nxe-widget nxe-tree-view" }, ngImport: i0, template: "@if (searchEnabled()) {\r\n <div class=\"nxe-treeview-search\">\r\n <input\r\n class=\"nxe-treeview-search-input\"\r\n type=\"text\"\r\n placeholder=\"Search\"\r\n [value]=\"searchText()\"\r\n (input)=\"onSearch($event)\" />\r\n </div>\r\n}\r\n\r\n<ul class=\"nxe-treeview-node-container nxe-treeview-node-container-opened\">\r\n @for (node of visibleNodes(); track node.key) {\r\n <li\r\n class=\"nxe-treeview-node\"\r\n [class.nxe-treeview-node-is-leaf]=\"!node.hasChildren\"\r\n [class.nxe-state-selected]=\"selectionMode() !== 'none' && node.key === selectedKey()\"\r\n [style.padding-left.px]=\"node.level * 15\">\r\n <div class=\"nxe-treeview-item\" (click)=\"onNodeClick(node, $event)\">\r\n @if (node.hasChildren) {\r\n <span\r\n class=\"nxe-treeview-toggle-item-visibility\"\r\n [class.nxe-treeview-toggle-item-visibility-opened]=\"isExpanded(node)\"\r\n (click)=\"toggle(node, $event)\">\r\n </span>\r\n } @else {\r\n <span class=\"nxe-treeview-expander-icon-stub\"></span>\r\n }\r\n <div class=\"nxe-treeview-item-content\">\r\n <span>{{ node.text }}</span>\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n</ul>\r\n", styles: ["nxe-tree-view{display:block;height:100%;overflow-y:auto;border:1px solid #ddd;border-radius:4px}nxe-tree-view .nxe-treeview-search{padding:4px 6px}nxe-tree-view .nxe-treeview-search .nxe-treeview-search-input{width:100%;box-sizing:border-box;padding:5px 8px;border:1px solid #ddd;border-radius:4px;font-size:1em;outline:none}nxe-tree-view .nxe-treeview-search .nxe-treeview-search-input:focus{border-color:#30374c}nxe-tree-view .nxe-treeview-node-container{list-style:none;margin:0;padding:0}nxe-tree-view .nxe-treeview-node{list-style:none}nxe-tree-view .nxe-treeview-item{display:flex;align-items:center;gap:4px;height:32px;box-sizing:border-box;padding:5px 8px 5px 10px;cursor:pointer;border-radius:4px}nxe-tree-view .nxe-treeview-item:hover{background-color:#0000000a}nxe-tree-view .nxe-treeview-node.nxe-state-selected>.nxe-treeview-item{background-color:#30374c1f;font-weight:500}nxe-tree-view .nxe-treeview-item-content{flex:1 1 auto;min-width:0}nxe-tree-view .nxe-treeview-item-content span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-tree-view .nxe-treeview-expander-icon-stub{display:inline-block;width:16px;height:16px;flex:0 0 16px}nxe-tree-view .nxe-treeview-toggle-item-visibility{display:inline-block;position:relative;width:16px;height:16px;flex:0 0 16px;cursor:pointer}nxe-tree-view .nxe-treeview-toggle-item-visibility:before{content:\"\";position:absolute;left:5px;top:4px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:5px solid #444;transition:transform .15s ease;transform-origin:40% 50%}nxe-tree-view .nxe-treeview-toggle-item-visibility.nxe-treeview-toggle-item-visibility-opened:before{transform:rotate(90deg)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
185
|
+
}
|
|
186
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTreeViewComponent, decorators: [{
|
|
187
|
+
type: Component,
|
|
188
|
+
args: [{ selector: 'nxe-tree-view', standalone: true, host: {
|
|
189
|
+
class: 'nxe-widget nxe-tree-view',
|
|
190
|
+
'[class.nxe-treeview-with-search]': 'searchEnabled()',
|
|
191
|
+
'[style.width]': 'widthStyle()',
|
|
192
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (searchEnabled()) {\r\n <div class=\"nxe-treeview-search\">\r\n <input\r\n class=\"nxe-treeview-search-input\"\r\n type=\"text\"\r\n placeholder=\"Search\"\r\n [value]=\"searchText()\"\r\n (input)=\"onSearch($event)\" />\r\n </div>\r\n}\r\n\r\n<ul class=\"nxe-treeview-node-container nxe-treeview-node-container-opened\">\r\n @for (node of visibleNodes(); track node.key) {\r\n <li\r\n class=\"nxe-treeview-node\"\r\n [class.nxe-treeview-node-is-leaf]=\"!node.hasChildren\"\r\n [class.nxe-state-selected]=\"selectionMode() !== 'none' && node.key === selectedKey()\"\r\n [style.padding-left.px]=\"node.level * 15\">\r\n <div class=\"nxe-treeview-item\" (click)=\"onNodeClick(node, $event)\">\r\n @if (node.hasChildren) {\r\n <span\r\n class=\"nxe-treeview-toggle-item-visibility\"\r\n [class.nxe-treeview-toggle-item-visibility-opened]=\"isExpanded(node)\"\r\n (click)=\"toggle(node, $event)\">\r\n </span>\r\n } @else {\r\n <span class=\"nxe-treeview-expander-icon-stub\"></span>\r\n }\r\n <div class=\"nxe-treeview-item-content\">\r\n <span>{{ node.text }}</span>\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n</ul>\r\n", styles: ["nxe-tree-view{display:block;height:100%;overflow-y:auto;border:1px solid #ddd;border-radius:4px}nxe-tree-view .nxe-treeview-search{padding:4px 6px}nxe-tree-view .nxe-treeview-search .nxe-treeview-search-input{width:100%;box-sizing:border-box;padding:5px 8px;border:1px solid #ddd;border-radius:4px;font-size:1em;outline:none}nxe-tree-view .nxe-treeview-search .nxe-treeview-search-input:focus{border-color:#30374c}nxe-tree-view .nxe-treeview-node-container{list-style:none;margin:0;padding:0}nxe-tree-view .nxe-treeview-node{list-style:none}nxe-tree-view .nxe-treeview-item{display:flex;align-items:center;gap:4px;height:32px;box-sizing:border-box;padding:5px 8px 5px 10px;cursor:pointer;border-radius:4px}nxe-tree-view .nxe-treeview-item:hover{background-color:#0000000a}nxe-tree-view .nxe-treeview-node.nxe-state-selected>.nxe-treeview-item{background-color:#30374c1f;font-weight:500}nxe-tree-view .nxe-treeview-item-content{flex:1 1 auto;min-width:0}nxe-tree-view .nxe-treeview-item-content span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-tree-view .nxe-treeview-expander-icon-stub{display:inline-block;width:16px;height:16px;flex:0 0 16px}nxe-tree-view .nxe-treeview-toggle-item-visibility{display:inline-block;position:relative;width:16px;height:16px;flex:0 0 16px;cursor:pointer}nxe-tree-view .nxe-treeview-toggle-item-visibility:before{content:\"\";position:absolute;left:5px;top:4px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:5px solid #444;transition:transform .15s ease;transform-origin:40% 50%}nxe-tree-view .nxe-treeview-toggle-item-visibility.nxe-treeview-toggle-item-visibility-opened:before{transform:rotate(90deg)}\n"] }]
|
|
193
|
+
}], propDecorators: { dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], dataStructure: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataStructure", required: false }] }], keyExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyExpr", required: false }] }], displayExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], parentIdExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentIdExpr", required: false }] }], rootValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootValue", required: false }] }], expandedExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedExpr", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selectByClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectByClick", required: false }] }], searchEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchEnabled", required: false }] }], searchMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchMode", required: false }] }], useNativeScrolling: [{ type: i0.Input, args: [{ isSignal: true, alias: "useNativeScrolling", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], onItemClick: [{ type: i0.Output, args: ["onItemClick"] }] } });
|
|
194
|
+
|
|
195
|
+
// @nexterajs/ui/tree-view — public API.
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generated bundle index. Do not edit.
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
export { NxeTreeViewComponent };
|
|
202
|
+
//# sourceMappingURL=nexterajs-ui-tree-view.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nexterajs-ui-tree-view.mjs","sources":["../../../projects/nextera-ui/tree-view/src/lib/tree-view/nxe-tree-view.component.ts","../../../projects/nextera-ui/tree-view/src/lib/tree-view/nxe-tree-view.component.html","../../../projects/nextera-ui/tree-view/src/public-api.ts","../../../projects/nextera-ui/tree-view/src/nexterajs-ui-tree-view.ts"],"sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n computed,\r\n input,\r\n output,\r\n signal,\r\n ViewEncapsulation,\r\n} from '@angular/core';\r\n\r\n/** A node in the assembled tree (children built from the flat `parentIdExpr` list). */\r\nexport interface NxeTreeNode {\r\n key: unknown;\r\n text: string;\r\n /** The original data row (consumers read `itemData` off the click event = this). */\r\n data: Record<string, unknown>;\r\n children: NxeTreeNode[];\r\n hasChildren: boolean;\r\n level: number;\r\n}\r\n\r\n/** DevExtreme-shaped `onItemClick` payload (consumers read `event.itemData.<field>`). */\r\nexport interface NxeTreeItemClickEvent {\r\n itemData: Record<string, unknown>;\r\n node: NxeTreeNode;\r\n event: Event;\r\n component: NxeTreeViewComponent;\r\n element: HTMLElement;\r\n}\r\n\r\n/**\r\n * `nxe-tree-view` — drop-in replacement for DevExtreme `dx-tree-view`.\r\n *\r\n * The single usage (settings sidebar) feeds a FLAT list (`dataStructure=\"plain\"`) and lets the\r\n * widget assemble the hierarchy via `keyExpr`/`parentIdExpr`/`rootValue`; so this builds the tree\r\n * internally. Single selection, `selectByClick`, optional search box, and a programmatic\r\n * `expandAll()` (called via `@ViewChild(NxeTreeViewComponent).instance.expandAll()` after async\r\n * load). `(onItemClick)` emits `{ itemData: <full original row>, ... }` so the navigation handler\r\n * keeps reading `event.itemData.url/urlParams/objectName`.\r\n */\r\n@Component({\r\n selector: 'nxe-tree-view',\r\n standalone: true,\r\n templateUrl: './nxe-tree-view.component.html',\r\n styleUrl: './nxe-tree-view.component.scss',\r\n host: {\r\n class: 'nxe-widget nxe-tree-view',\r\n '[class.nxe-treeview-with-search]': 'searchEnabled()',\r\n '[style.width]': 'widthStyle()',\r\n },\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.None,\r\n})\r\nexport class NxeTreeViewComponent {\r\n readonly dataSource = input<readonly unknown[] | null | undefined>([]);\r\n /** Accepted for parity; this component always assembles a tree from the flat list. */\r\n readonly dataStructure = input<'plain' | 'tree'>('plain');\r\n readonly keyExpr = input<string>('id');\r\n readonly displayExpr = input<string>('text');\r\n readonly parentIdExpr = input<string>('parentId');\r\n readonly rootValue = input<unknown>(0);\r\n /** Accepted for parity (no-op: rows carry no `expanded` field; use `expandAll()`). */\r\n readonly expandedExpr = input<string>('expanded');\r\n readonly selectionMode = input<'single' | 'multiple' | 'none'>('single');\r\n readonly selectByClick = input(false);\r\n readonly searchEnabled = input(false);\r\n readonly searchMode = input<'contains' | 'startswith' | 'equals'>('contains');\r\n /** Accepted for parity (native scrolling is the default here anyway). */\r\n readonly useNativeScrolling = input(true);\r\n readonly width = input<number | string | undefined>(undefined);\r\n\r\n readonly onItemClick = output<NxeTreeItemClickEvent>();\r\n\r\n readonly expanded = signal<ReadonlySet<unknown>>(new Set());\r\n readonly selectedKey = signal<unknown>(null);\r\n readonly searchText = signal('');\r\n\r\n /** DevExtreme `.instance` facade — `expandAll()` is the only method used. */\r\n get instance(): this {\r\n return this;\r\n }\r\n\r\n protected readonly widthStyle = computed(() => {\r\n const w = this.width();\r\n return typeof w === 'number' ? `${w}px` : w;\r\n });\r\n\r\n private readonly tree = computed<NxeTreeNode[]>(() => {\r\n const rows = (this.dataSource() ?? []) as Record<string, unknown>[];\r\n const keyF = this.keyExpr();\r\n const parentF = this.parentIdExpr();\r\n const dispF = this.displayExpr();\r\n const root = this.rootValue();\r\n\r\n const byKey = new Map<unknown, NxeTreeNode>();\r\n for (const r of rows) {\r\n byKey.set(r[keyF], {\r\n key: r[keyF],\r\n text: String(r[dispF] ?? ''),\r\n data: r,\r\n children: [],\r\n hasChildren: false,\r\n level: 0,\r\n });\r\n }\r\n const roots: NxeTreeNode[] = [];\r\n for (const r of rows) {\r\n const node = byKey.get(r[keyF])!;\r\n const parentKey = r[parentF];\r\n const parent = parentKey !== root ? byKey.get(parentKey) : undefined;\r\n if (parent) {\r\n parent.children.push(node);\r\n parent.hasChildren = true;\r\n } else {\r\n roots.push(node);\r\n }\r\n }\r\n const assignLevels = (nodes: NxeTreeNode[], level: number): void => {\r\n for (const n of nodes) {\r\n n.level = level;\r\n assignLevels(n.children, level + 1);\r\n }\r\n };\r\n assignLevels(roots, 0);\r\n return roots;\r\n });\r\n\r\n /** Flattened list of currently-visible nodes (respects expand state + search). */\r\n protected readonly visibleNodes = computed<NxeTreeNode[]>(() => {\r\n const out: NxeTreeNode[] = [];\r\n const query = this.searchText().trim().toLowerCase();\r\n if (query) {\r\n const match = (text: string): boolean => {\r\n const t = text.toLowerCase();\r\n switch (this.searchMode()) {\r\n case 'startswith':\r\n return t.startsWith(query);\r\n case 'equals':\r\n return t === query;\r\n default:\r\n return t.includes(query);\r\n }\r\n };\r\n const walkAll = (nodes: NxeTreeNode[]): void => {\r\n for (const n of nodes) {\r\n if (match(n.text)) {\r\n out.push(n);\r\n }\r\n walkAll(n.children);\r\n }\r\n };\r\n walkAll(this.tree());\r\n } else {\r\n const expanded = this.expanded();\r\n const walk = (nodes: NxeTreeNode[]): void => {\r\n for (const n of nodes) {\r\n out.push(n);\r\n if (n.hasChildren && expanded.has(n.key)) {\r\n walk(n.children);\r\n }\r\n }\r\n };\r\n walk(this.tree());\r\n }\r\n return out;\r\n });\r\n\r\n expandAll(): void {\r\n const keys = new Set<unknown>();\r\n const walk = (nodes: NxeTreeNode[]): void => {\r\n for (const n of nodes) {\r\n if (n.hasChildren) {\r\n keys.add(n.key);\r\n }\r\n walk(n.children);\r\n }\r\n };\r\n walk(this.tree());\r\n this.expanded.set(keys);\r\n }\r\n\r\n collapseAll(): void {\r\n this.expanded.set(new Set());\r\n }\r\n\r\n protected isExpanded(node: NxeTreeNode): boolean {\r\n return this.expanded().has(node.key);\r\n }\r\n\r\n protected toggle(node: NxeTreeNode, event: Event): void {\r\n event.stopPropagation();\r\n this.expanded.update((set) => {\r\n const next = new Set(set);\r\n next.has(node.key) ? next.delete(node.key) : next.add(node.key);\r\n return next;\r\n });\r\n }\r\n\r\n protected onNodeClick(node: NxeTreeNode, event: Event): void {\r\n if (this.selectByClick() && this.selectionMode() !== 'none') {\r\n this.selectedKey.set(node.key);\r\n }\r\n this.onItemClick.emit({\r\n itemData: node.data,\r\n node,\r\n event,\r\n component: this,\r\n element: event.currentTarget as HTMLElement,\r\n });\r\n }\r\n\r\n protected onSearch(event: Event): void {\r\n this.searchText.set((event.target as HTMLInputElement).value);\r\n }\r\n}\r\n","@if (searchEnabled()) {\r\n <div class=\"nxe-treeview-search\">\r\n <input\r\n class=\"nxe-treeview-search-input\"\r\n type=\"text\"\r\n placeholder=\"Search\"\r\n [value]=\"searchText()\"\r\n (input)=\"onSearch($event)\" />\r\n </div>\r\n}\r\n\r\n<ul class=\"nxe-treeview-node-container nxe-treeview-node-container-opened\">\r\n @for (node of visibleNodes(); track node.key) {\r\n <li\r\n class=\"nxe-treeview-node\"\r\n [class.nxe-treeview-node-is-leaf]=\"!node.hasChildren\"\r\n [class.nxe-state-selected]=\"selectionMode() !== 'none' && node.key === selectedKey()\"\r\n [style.padding-left.px]=\"node.level * 15\">\r\n <div class=\"nxe-treeview-item\" (click)=\"onNodeClick(node, $event)\">\r\n @if (node.hasChildren) {\r\n <span\r\n class=\"nxe-treeview-toggle-item-visibility\"\r\n [class.nxe-treeview-toggle-item-visibility-opened]=\"isExpanded(node)\"\r\n (click)=\"toggle(node, $event)\">\r\n </span>\r\n } @else {\r\n <span class=\"nxe-treeview-expander-icon-stub\"></span>\r\n }\r\n <div class=\"nxe-treeview-item-content\">\r\n <span>{{ node.text }}</span>\r\n </div>\r\n </div>\r\n </li>\r\n }\r\n</ul>\r\n","// @nexterajs/ui/tree-view — public API.\r\nexport * from './lib/tree-view/nxe-tree-view.component';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, inject, contentChildren, signal, Injector, DestroyRef } from '@angular/core';
|
|
3
|
+
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { distinctUntilChanged, startWith, pairwise } from 'rxjs';
|
|
5
|
+
import { NXE_VALIDATION_TARGET, NxeValidationGroupRegistry } from '@nexterajs/ui/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The message shown when a rule fails and the caller gave none.
|
|
9
|
+
*
|
|
10
|
+
* These are DevExtreme's own defaults, verbatim from its `validation-*` message keys. Without
|
|
11
|
+
* them a rule with no `message` blocks its group and displays nothing at all, because every
|
|
12
|
+
* editor renders its error bubble under `@if (validationError())` — falsy for the empty string.
|
|
13
|
+
*/
|
|
14
|
+
const NXE_DEFAULT_VALIDATION_MESSAGES = {
|
|
15
|
+
required: 'Required',
|
|
16
|
+
email: 'Email is invalid',
|
|
17
|
+
pattern: 'Value does not match pattern',
|
|
18
|
+
range: 'Value is out of range',
|
|
19
|
+
compare: 'Values do not match',
|
|
20
|
+
custom: 'Value is invalid',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* `nxe-validation-rule` — drop-in for DevExtreme `dxi-validation-rule`.
|
|
24
|
+
*
|
|
25
|
+
* A no-template configuration child of `nxe-validator`. The codebase uses the universal element
|
|
26
|
+
* with a `type` attribute (overwhelmingly `type="required"`), so a single component with a
|
|
27
|
+
* `type` input covers 100% of usage.
|
|
28
|
+
*/
|
|
29
|
+
class NxeValidationRuleComponent {
|
|
30
|
+
type = input('required', /* @ts-ignore */
|
|
31
|
+
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
32
|
+
message = input('', /* @ts-ignore */
|
|
33
|
+
...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
|
|
34
|
+
/** `compare` rule: returns the value to compare against. Mirrors `comparisonTarget`. */
|
|
35
|
+
comparisonTarget = input(undefined, /* @ts-ignore */
|
|
36
|
+
...(ngDevMode ? [{ debugName: "comparisonTarget" }] : /* istanbul ignore next */ []));
|
|
37
|
+
/** `compare` rule comparison operator (default `==`). */
|
|
38
|
+
comparisonType = input('==', /* @ts-ignore */
|
|
39
|
+
...(ngDevMode ? [{ debugName: "comparisonType" }] : /* istanbul ignore next */ []));
|
|
40
|
+
/** `custom` rule: returns true when valid. Mirrors `validationCallback`. */
|
|
41
|
+
validationCallback = input(undefined, /* @ts-ignore */
|
|
42
|
+
...(ngDevMode ? [{ debugName: "validationCallback" }] : /* istanbul ignore next */ []));
|
|
43
|
+
/** `pattern` rule. */
|
|
44
|
+
pattern = input(undefined, /* @ts-ignore */
|
|
45
|
+
...(ngDevMode ? [{ debugName: "pattern" }] : /* istanbul ignore next */ []));
|
|
46
|
+
/** `range` rule bounds (DevExtreme RangeRule: empty values pass, bounds are inclusive). */
|
|
47
|
+
min = input(undefined, /* @ts-ignore */
|
|
48
|
+
...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
49
|
+
max = input(undefined, /* @ts-ignore */
|
|
50
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
51
|
+
/**
|
|
52
|
+
* The message to show when this rule fails: the caller's, or DevExtreme's default for the
|
|
53
|
+
* rule type. Whitespace counts as absent — a message of spaces is still an empty bubble.
|
|
54
|
+
*/
|
|
55
|
+
resolvedMessage = computed(() => this.message().trim() || NXE_DEFAULT_VALIDATION_MESSAGES[this.type()] || '', /* @ts-ignore */
|
|
56
|
+
...(ngDevMode ? [{ debugName: "resolvedMessage" }] : /* istanbul ignore next */ []));
|
|
57
|
+
/** Whether `value` satisfies this rule. */
|
|
58
|
+
evaluate(value) {
|
|
59
|
+
switch (this.type()) {
|
|
60
|
+
case 'required':
|
|
61
|
+
return !this.isEmpty(value);
|
|
62
|
+
case 'compare':
|
|
63
|
+
return this.compare(value);
|
|
64
|
+
case 'range':
|
|
65
|
+
return this.isEmpty(value) || this.inRange(value);
|
|
66
|
+
case 'pattern':
|
|
67
|
+
return this.isEmpty(value) || this.matchesPattern(value);
|
|
68
|
+
case 'email':
|
|
69
|
+
return this.isEmpty(value) || /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(value));
|
|
70
|
+
case 'custom':
|
|
71
|
+
return this.validationCallback()?.({ value, rule: this }) ?? true;
|
|
72
|
+
default:
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** DevExtreme RequiredRule semantics: null/undefined/''/false are "not specified". */
|
|
77
|
+
isEmpty(value) {
|
|
78
|
+
return (value === null ||
|
|
79
|
+
value === undefined ||
|
|
80
|
+
value === '' ||
|
|
81
|
+
value === false ||
|
|
82
|
+
(Array.isArray(value) && value.length === 0));
|
|
83
|
+
}
|
|
84
|
+
compare(value) {
|
|
85
|
+
const target = this.comparisonTarget()?.();
|
|
86
|
+
switch (this.comparisonType()) {
|
|
87
|
+
case '===':
|
|
88
|
+
return value === target;
|
|
89
|
+
case '!=':
|
|
90
|
+
// eslint-disable-next-line eqeqeq
|
|
91
|
+
return value != target;
|
|
92
|
+
case '!==':
|
|
93
|
+
return value !== target;
|
|
94
|
+
case '>':
|
|
95
|
+
return value > target;
|
|
96
|
+
case '<':
|
|
97
|
+
return value < target;
|
|
98
|
+
case '>=':
|
|
99
|
+
return value >= target;
|
|
100
|
+
case '<=':
|
|
101
|
+
return value <= target;
|
|
102
|
+
case '==':
|
|
103
|
+
default:
|
|
104
|
+
// eslint-disable-next-line eqeqeq
|
|
105
|
+
return value == target;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
inRange(value) {
|
|
109
|
+
const num = Number(value);
|
|
110
|
+
if (Number.isNaN(num)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const min = this.min();
|
|
114
|
+
const max = this.max();
|
|
115
|
+
return (min === undefined || num >= min) && (max === undefined || num <= max);
|
|
116
|
+
}
|
|
117
|
+
matchesPattern(value) {
|
|
118
|
+
const pattern = this.pattern();
|
|
119
|
+
if (!pattern) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
123
|
+
return regex.test(String(value));
|
|
124
|
+
}
|
|
125
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeValidationRuleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
126
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeValidationRuleComponent, isStandalone: true, selector: "nxe-validation-rule", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, comparisonTarget: { classPropertyName: "comparisonTarget", publicName: "comparisonTarget", isSignal: true, isRequired: false, transformFunction: null }, comparisonType: { classPropertyName: "comparisonType", publicName: "comparisonType", isSignal: true, isRequired: false, transformFunction: null }, validationCallback: { classPropertyName: "validationCallback", publicName: "validationCallback", isSignal: true, isRequired: false, transformFunction: null }, pattern: { classPropertyName: "pattern", publicName: "pattern", 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 } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
127
|
+
}
|
|
128
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeValidationRuleComponent, decorators: [{
|
|
129
|
+
type: Component,
|
|
130
|
+
args: [{
|
|
131
|
+
selector: 'nxe-validation-rule',
|
|
132
|
+
standalone: true,
|
|
133
|
+
template: '',
|
|
134
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
135
|
+
encapsulation: ViewEncapsulation.None,
|
|
136
|
+
}]
|
|
137
|
+
}], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], comparisonTarget: [{ type: i0.Input, args: [{ isSignal: true, alias: "comparisonTarget", required: false }] }], comparisonType: [{ type: i0.Input, args: [{ isSignal: true, alias: "comparisonType", required: false }] }], validationCallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationCallback", required: false }] }], pattern: [{ type: i0.Input, args: [{ isSignal: true, alias: "pattern", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }] } });
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* `nxe-validator` — drop-in for DevExtreme `dx-validator`.
|
|
141
|
+
*
|
|
142
|
+
* Placed inside an editor (`<nxe-text-box><nxe-validator><nxe-validation-rule .../></nxe-validator></nxe-text-box>`),
|
|
143
|
+
* it adapts the editor via {@link NXE_VALIDATION_TARGET}, collects its `nxe-validation-rule`
|
|
144
|
+
* children, and registers into the {@link NxeValidationGroupRegistry} under the editor's
|
|
145
|
+
* `validationGroup`. Validation is on-demand (matching DevExtreme: not auto-run on change) —
|
|
146
|
+
* `validate()` is invoked by `nxe-button[validationGroup]` (group-level) or directly.
|
|
147
|
+
*/
|
|
148
|
+
class NxeValidatorComponent {
|
|
149
|
+
target = inject(NXE_VALIDATION_TARGET, { optional: true });
|
|
150
|
+
registry = inject(NxeValidationGroupRegistry);
|
|
151
|
+
rules = contentChildren(NxeValidationRuleComponent, /* @ts-ignore */
|
|
152
|
+
...(ngDevMode ? [{ debugName: "rules" }] : /* istanbul ignore next */ []));
|
|
153
|
+
/** Explicit group; otherwise inherited from the wrapped editor's `validationGroup`. */
|
|
154
|
+
validationGroupInput = input(undefined, { ...(ngDevMode ? { debugName: "validationGroupInput" } : /* istanbul ignore next */ {}), alias: 'validationGroup' });
|
|
155
|
+
// Bare validators (no group) join the implicit default group `''` — mirroring DevExtreme's
|
|
156
|
+
// global default group — so a groupless `nxe-button` validates them.
|
|
157
|
+
group = computed(() => this.validationGroupInput() ?? this.target?.validationGroup() ?? '', /* @ts-ignore */
|
|
158
|
+
...(ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []));
|
|
159
|
+
/** Last validation outcome (false once a rule has failed, until re-validated). */
|
|
160
|
+
isValid = signal(true, /* @ts-ignore */
|
|
161
|
+
...(ngDevMode ? [{ debugName: "isValid" }] : /* istanbul ignore next */ []));
|
|
162
|
+
/** DevExtreme parity: existing code calls `validator.instance.validate()` via `@ViewChild`. */
|
|
163
|
+
get instance() {
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
member = { validate: () => this.validate() };
|
|
167
|
+
constructor() {
|
|
168
|
+
// Register/unregister into the named group reactively (handles dynamic group changes).
|
|
169
|
+
/* ⚠ NOT an effect - UI-48. The registration follows ONE signal, and the unregister that
|
|
170
|
+
`onCleanup` used to run is the pairwise step below: a group change unregisters the old
|
|
171
|
+
one before registering the new, and destruction unregisters the last. */
|
|
172
|
+
toObservable(this.group, { injector: inject(Injector) })
|
|
173
|
+
.pipe(distinctUntilChanged(), startWith(undefined), pairwise(), takeUntilDestroyed())
|
|
174
|
+
.subscribe(([previous, group]) => {
|
|
175
|
+
if (previous !== undefined) {
|
|
176
|
+
this.registry.unregister(previous, this.member);
|
|
177
|
+
}
|
|
178
|
+
this.registry.register(group, this.member);
|
|
179
|
+
});
|
|
180
|
+
inject(DestroyRef).onDestroy(() => this.registry.unregister(this.group(), this.member));
|
|
181
|
+
}
|
|
182
|
+
/** Run all rules against the editor's current value; pushes the first failing message. */
|
|
183
|
+
validate() {
|
|
184
|
+
const value = this.target?.getValue();
|
|
185
|
+
for (const rule of this.rules()) {
|
|
186
|
+
if (!rule.evaluate(value)) {
|
|
187
|
+
// resolvedMessage(), not message(): a rule declared without one would otherwise
|
|
188
|
+
// push the empty string, and every editor guards its bubble with
|
|
189
|
+
// `@if (validationError())` — so the field would block the group in silence.
|
|
190
|
+
const message = rule.resolvedMessage();
|
|
191
|
+
this.isValid.set(false);
|
|
192
|
+
this.target?.setValidationError(message);
|
|
193
|
+
return {
|
|
194
|
+
isValid: false,
|
|
195
|
+
brokenRules: [{ type: rule.type(), message }],
|
|
196
|
+
status: 'invalid',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
this.isValid.set(true);
|
|
201
|
+
this.target?.setValidationError(null);
|
|
202
|
+
return { isValid: true, brokenRules: [], status: 'valid' };
|
|
203
|
+
}
|
|
204
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeValidatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
205
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeValidatorComponent, isStandalone: true, selector: "nxe-validator", inputs: { validationGroupInput: { classPropertyName: "validationGroupInput", publicName: "validationGroup", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rules", predicate: NxeValidationRuleComponent, isSignal: true }], ngImport: i0, template: '<ng-content />', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
206
|
+
}
|
|
207
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeValidatorComponent, decorators: [{
|
|
208
|
+
type: Component,
|
|
209
|
+
args: [{
|
|
210
|
+
selector: 'nxe-validator',
|
|
211
|
+
standalone: true,
|
|
212
|
+
template: '<ng-content />',
|
|
213
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
214
|
+
encapsulation: ViewEncapsulation.None,
|
|
215
|
+
}]
|
|
216
|
+
}], ctorParameters: () => [], propDecorators: { rules: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeValidationRuleComponent), { isSignal: true }] }], validationGroupInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationGroup", required: false }] }] } });
|
|
217
|
+
|
|
218
|
+
class NxeValidators {
|
|
219
|
+
/**
|
|
220
|
+
* Check for empty (optional fields) values
|
|
221
|
+
*
|
|
222
|
+
* @param value
|
|
223
|
+
*/
|
|
224
|
+
static isEmptyInputValue(value) {
|
|
225
|
+
return value == null || value.length === 0;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Must match validator
|
|
229
|
+
*
|
|
230
|
+
* @param controlPath A dot-delimited string values that define the path to the control.
|
|
231
|
+
* @param matchingControlPath A dot-delimited string values that define the path to the matching control.
|
|
232
|
+
*/
|
|
233
|
+
static mustMatch(controlPath, matchingControlPath) {
|
|
234
|
+
return (formGroup) => {
|
|
235
|
+
// Get the control and matching control
|
|
236
|
+
const control = formGroup.get(controlPath);
|
|
237
|
+
const matchingControl = formGroup.get(matchingControlPath);
|
|
238
|
+
// Return if control or matching control doesn't exist
|
|
239
|
+
if (!control || !matchingControl) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
// Delete the mustMatch error to reset the error on the matching control
|
|
243
|
+
if (matchingControl.hasError('mustMatch')) {
|
|
244
|
+
delete matchingControl.errors.mustMatch;
|
|
245
|
+
matchingControl.updateValueAndValidity();
|
|
246
|
+
}
|
|
247
|
+
// Don't validate empty values on the matching control
|
|
248
|
+
// Don't validate if values are matching
|
|
249
|
+
if (this.isEmptyInputValue(matchingControl.value) || control.value === matchingControl.value) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
// Prepare the validation errors
|
|
253
|
+
const errors = { mustMatch: true };
|
|
254
|
+
// Set the validation error on the matching control
|
|
255
|
+
matchingControl.setErrors(errors);
|
|
256
|
+
// Return the errors
|
|
257
|
+
return errors;
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// @nexterajs/ui/validation — public API.
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Generated bundle index. Do not edit.
|
|
266
|
+
*/
|
|
267
|
+
|
|
268
|
+
export { NXE_DEFAULT_VALIDATION_MESSAGES, NxeValidationRuleComponent, NxeValidatorComponent, NxeValidators };
|
|
269
|
+
//# sourceMappingURL=nexterajs-ui-validation.mjs.map
|