@acorex/cdk 22.1.0-next.2 → 22.1.0-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { model, output, Component, inject, ElementRef, signal, afterNextRender, Injectable, ChangeDetectorRef, ViewContainerRef, EventEmitter, DOCUMENT, PLATFORM_ID, Renderer2, Input, Directive, NgModule, InjectionToken, Output, input, effect } from '@angular/core';
2
+ import { model, output, Component, inject, ElementRef, signal, afterNextRender, Injectable, EventEmitter, PLATFORM_ID, ChangeDetectorRef, ViewContainerRef, DOCUMENT as DOCUMENT$1, Renderer2, Input, Directive, NgModule, InjectionToken, Output, input, effect } from '@angular/core';
3
3
  import { AXHtmlUtil } from '@acorex/core/utils';
4
- import { flatten, cloneDeep, isEqual, omit, get } from 'lodash-es';
5
4
  import { isBrowser } from '@acorex/core/platform';
6
5
  import { AXValidationService } from '@acorex/core/validation';
6
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
7
+ import { omit, isEqual, flatten, cloneDeep, get } from 'lodash-es';
7
8
  import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
8
- import { isPlatformBrowser } from '@angular/common';
9
9
  import { noop, Subscription, BehaviorSubject, debounceTime, distinctUntilChanged, Subject, fromEvent, skip } from 'rxjs';
10
10
  import { DataSource } from '@angular/cdk/collections';
11
11
  import { EventManager } from '@angular/platform-browser';
@@ -66,6 +66,13 @@ class NXComponent {
66
66
  get nativeElement() {
67
67
  return this.#elementRef.nativeElement;
68
68
  }
69
+ /**
70
+ * Gets the host HTML element of the component.
71
+ * Same contract as classic editors so `ax-label` / `ax-form-field` can associate the field.
72
+ */
73
+ getHostElement() {
74
+ return this.nativeElement;
75
+ }
69
76
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NXComponent, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
70
77
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NXComponent }); }
71
78
  }
@@ -136,25 +143,155 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
136
143
  }]
137
144
  }], propDecorators: { look: [{ type: i0.Input, args: [{ isSignal: true, alias: "look", required: false }] }, { type: i0.Output, args: ["lookChange"] }] } });
138
145
 
139
- class NXValueComponent {
146
+ /**
147
+ * Signal-based value editor with the same `ax-validation-rule` / `ax-form` contract as the classic value mixins.
148
+ */
149
+ class NXValueComponent extends NXComponent {
140
150
  constructor() {
141
- this.readonly = model(/* @ts-ignore */
142
- ...(ngDevMode ? [undefined, { debugName: "readonly" }] : /* istanbul ignore next */ []));
143
- this.name = model(/* @ts-ignore */
144
- ...(ngDevMode ? [undefined, { debugName: "name" }] : /* istanbul ignore next */ []));
145
- this.value = model(/* @ts-ignore */
146
- ...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
147
- this.onValueChanged = output();
148
- }
149
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NXValueComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
150
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: NXValueComponent, isStandalone: true, selector: "ng-component", inputs: { readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { readonly: "readonlyChange", name: "nameChange", value: "valueChange", onValueChanged: "onValueChanged" }, ngImport: i0, template: '', isInline: true }); }
151
+ super(...arguments);
152
+ this.validationRulesChange = new EventEmitter();
153
+ this.#validationRules = [];
154
+ this.#state = 'clear';
155
+ this.#validationService = inject(AXValidationService);
156
+ this.#document = inject(DOCUMENT);
157
+ this.#platformId = inject(PLATFORM_ID);
158
+ this.#registerValidation = afterNextRender(() => {
159
+ this.registerValidation();
160
+ });
161
+ }
162
+ #validationRules;
163
+ #state;
164
+ #validationService;
165
+ #document;
166
+ #platformId;
167
+ #registerValidation;
168
+ get isRequired() {
169
+ return this.#validationRules.some((rule) => rule.rule == 'required');
170
+ }
171
+ get state() {
172
+ return this.#state;
173
+ }
174
+ registerValidation() {
175
+ this.nativeElement.querySelectorAll('[ax-form-item="true"]').forEach((node) => {
176
+ node.removeAttribute('ax-form-item');
177
+ });
178
+ this.nativeElement.setAttribute('ax-form-item', 'true');
179
+ }
180
+ addValidationRule(rule) {
181
+ this.#validationRules.push(rule);
182
+ this.validationRulesChange.emit(this.#validationRules);
183
+ }
184
+ removeValidationRule(ruleToRemove) {
185
+ this.#validationRules = this.#validationRules.filter((rule) => {
186
+ if (rule.rule !== ruleToRemove.rule) {
187
+ return true;
188
+ }
189
+ const ruleOptions = omit(rule.options, ['title']);
190
+ const removeOptions = omit(ruleToRemove.options, ['title']);
191
+ return !isEqual(ruleOptions['pattern'], removeOptions['pattern']);
192
+ });
193
+ this.validationRulesChange.emit(this.#validationRules);
194
+ this.setState('clear');
195
+ }
196
+ removeValidationRuleByType(type) {
197
+ this.#validationRules = this.#validationRules.filter((rule) => rule.rule !== type);
198
+ this.validationRulesChange.emit(this.#validationRules);
199
+ this.setState('clear');
200
+ }
201
+ async validate() {
202
+ const containerClasses = ['ax-editor-container', 'ax-checkbox', 'ax-radio'];
203
+ const container = containerClasses.some((className) => this.nativeElement.classList.contains(className))
204
+ ? this.nativeElement
205
+ : this.nativeElement.querySelector('.ax-editor-container');
206
+ const formField = (container || this.nativeElement).closest('ax-form-field') || container?.closest('ax-checkbox');
207
+ const label = formField?.querySelector('ax-label') || formField?.querySelector('label');
208
+ const rules = this.#validationService.ruleFor(this.#currentValue());
209
+ this.#validationRules.forEach((rule) => {
210
+ rule.options ??= {};
211
+ rule.options['title'] = (label?.firstChild?.textContent ?? this.#currentName())?.replace('*', '').trim();
212
+ rules.addRule(rule.rule, rule.options);
213
+ });
214
+ const result = await rules.validate();
215
+ if (result.result) {
216
+ this.setState('clear');
217
+ }
218
+ else {
219
+ const message = result.rules.find((rule) => !rule.result)?.message;
220
+ this.setState('error', message);
221
+ }
222
+ return result;
223
+ }
224
+ setState(state, ...args) {
225
+ if (!isBrowser()) {
226
+ return;
227
+ }
228
+ const container = this.nativeElement.classList.contains('ax-editor-container')
229
+ ? this.nativeElement
230
+ : this.nativeElement.querySelector('.ax-editor-container');
231
+ const formField = (container || this.nativeElement).closest('ax-form-field');
232
+ const label = formField?.querySelector('ax-label') || formField?.querySelector('label');
233
+ let target = container || this.nativeElement.firstElementChild;
234
+ if (target?.querySelector('.ax-error-container')) {
235
+ target = target.querySelector('.ax-error-container');
236
+ }
237
+ const parent = target?.parentElement;
238
+ if (!target || !parent) {
239
+ return;
240
+ }
241
+ switch (state) {
242
+ case 'clear':
243
+ this.#state = 'clear';
244
+ parent.querySelector('span.ax-error-message')?.remove();
245
+ label?.classList.remove('ax-state-error');
246
+ target.classList.remove('ax-state-error', 'ax-state-success');
247
+ break;
248
+ case 'success':
249
+ this.setState('clear');
250
+ target.classList.add('ax-state-success');
251
+ this.#state = 'success';
252
+ break;
253
+ case 'error':
254
+ this.setState('clear');
255
+ if (isPlatformBrowser(this.#platformId)) {
256
+ const message = args[0] ?? '';
257
+ if (message) {
258
+ const span = this.#document.createElement('span');
259
+ span.innerHTML = message;
260
+ span.classList.add('ax-error-message');
261
+ parent.appendChild(span);
262
+ }
263
+ }
264
+ label?.classList.add('ax-state-error');
265
+ target.classList.add('ax-state-error');
266
+ this.#state = 'error';
267
+ break;
268
+ }
269
+ }
270
+ resetErrors() {
271
+ this.setState('clear');
272
+ }
273
+ commitValue(value, _isUserInteraction = false) {
274
+ const model = this.#valueModel();
275
+ model?.set?.(value);
276
+ }
277
+ #currentValue() {
278
+ return this.#resolve(this.value);
279
+ }
280
+ #currentName() {
281
+ return String(this.#resolve(this.name) ?? '');
282
+ }
283
+ #valueModel() {
284
+ return this.value;
285
+ }
286
+ #resolve(prop) {
287
+ return typeof prop === 'function' ? prop() : prop;
288
+ }
289
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NXValueComponent, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
290
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NXValueComponent }); }
151
291
  }
152
292
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NXValueComponent, decorators: [{
153
- type: Component,
154
- args: [{
155
- template: '',
156
- }]
157
- }], propDecorators: { readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }, { type: i0.Output, args: ["readonlyChange"] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }, { type: i0.Output, args: ["nameChange"] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onValueChanged: [{ type: i0.Output, args: ["onValueChanged"] }] } });
293
+ type: Injectable
294
+ }] });
158
295
 
159
296
  const defaultOffset = 3;
160
297
  function convertToPlacement(value) {
@@ -575,7 +712,7 @@ class MXValueComponent extends MXInteractiveComponent {
575
712
  this._isUserInteraction = false;
576
713
  this.stateChange = new EventEmitter();
577
714
  this._state = 'clear';
578
- this.document = inject(DOCUMENT);
715
+ this.document = inject(DOCUMENT$1);
579
716
  this.platformID = inject(PLATFORM_ID);
580
717
  this.validationService = inject(AXValidationService);
581
718
  this.validationRulesChange = new EventEmitter();
@@ -1047,6 +1184,10 @@ class AXListDataSource extends DataSource {
1047
1184
  .pipe(distinctUntilChanged())
1048
1185
  .subscribe((range) => {
1049
1186
  this._range = range;
1187
+ if (range.end <= range.start) {
1188
+ this.source.setPage(0);
1189
+ return;
1190
+ }
1050
1191
  const startPage = this.getPageForIndex(range.start);
1051
1192
  const endPage = this.getPageForIndex(range.end - 1);
1052
1193
  for (let i = startPage; i <= endPage; i++) {
@@ -1094,6 +1235,7 @@ class AXDataSource {
1094
1235
  this._totalCount = 0;
1095
1236
  this._items = [];
1096
1237
  this.fetchedPages = new Set();
1238
+ this.inflightPages = new Set();
1097
1239
  this._page = 0;
1098
1240
  this._query = { skip: 0, take: 0, sort: [] };
1099
1241
  this.onChanged = new Subject();
@@ -1106,24 +1248,35 @@ class AXDataSource {
1106
1248
  this._items = new Array(0);
1107
1249
  this._query.take = this.config.pageSize;
1108
1250
  }
1251
+ pageHasCachedItems(page) {
1252
+ const start = page * this.config.pageSize;
1253
+ return this._cachedItems.slice(start, start + this.config.pageSize).some((item) => item != null);
1254
+ }
1109
1255
  load() {
1110
1256
  const page = this._page;
1111
- if (this.fetchedPages.has(page) && this.useCache) {
1257
+ if (this.inflightPages.has(page)) {
1258
+ return;
1259
+ }
1260
+ if (this.fetchedPages.has(page) && this.useCache && this.pageHasCachedItems(page)) {
1112
1261
  return;
1113
1262
  }
1114
1263
  this.setLoadingState(true);
1115
- this.fetchedPages.add(page);
1264
+ this.inflightPages.add(page);
1116
1265
  this._query.take = this.config.pageSize;
1117
1266
  this._query.skip = page * this.config.pageSize;
1118
1267
  this.config
1119
1268
  .load({ ...this._query })
1120
1269
  .then((result) => {
1121
- if (this._totalCount != result.total) {
1122
- this._cachedItems = new Array(result.total);
1123
- this._totalCount = result.total;
1124
- }
1125
- this._items = result.items;
1126
- this._cachedItems.splice(page * this.config.pageSize, this.config.pageSize, ...result.items);
1270
+ const items = Array.isArray(result?.items) ? result.items : [];
1271
+ const reportedTotal = Number(result?.total);
1272
+ const total = Number.isFinite(reportedTotal) && reportedTotal > 0
1273
+ ? Math.max(reportedTotal, items.length)
1274
+ : items.length;
1275
+ this.fetchedPages.add(page);
1276
+ this._totalCount = total;
1277
+ this._cachedItems = new Array(Math.max(total, 0));
1278
+ this._items = items;
1279
+ this._cachedItems.splice(page * this.config.pageSize, this.config.pageSize, ...items);
1127
1280
  const event = {
1128
1281
  cachedItems: this.cachedItems,
1129
1282
  items: this.items,
@@ -1132,8 +1285,12 @@ class AXDataSource {
1132
1285
  totalPages: this.totalPages,
1133
1286
  };
1134
1287
  this.onChanged.next(event);
1288
+ })
1289
+ .catch(() => {
1290
+ this.fetchedPages.delete(page);
1135
1291
  })
1136
1292
  .finally(() => {
1293
+ this.inflightPages.delete(page);
1137
1294
  this.setLoadingState(false);
1138
1295
  });
1139
1296
  }
@@ -1161,15 +1318,16 @@ class AXDataSource {
1161
1318
  this._query.sort = value;
1162
1319
  }
1163
1320
  clearFilter() {
1164
- this._query.filter = null;
1321
+ delete this._query.filter;
1165
1322
  }
1166
1323
  reset() {
1167
- this._query.filter = null;
1324
+ this._query.filter = undefined;
1168
1325
  this._query.sort = [];
1169
1326
  this._query.skip = 0;
1170
1327
  this._items = new Array(0);
1171
1328
  this._cachedItems = new Array(this.config.pageSize);
1172
1329
  this.fetchedPages.clear();
1330
+ this.inflightPages.clear();
1173
1331
  this._page = 0;
1174
1332
  this._totalCount = 0;
1175
1333
  }
@@ -1263,7 +1421,7 @@ class AXAutoFocusDirective {
1263
1421
  this._host = inject(AXFocusableComponent, { host: true, self: true, optional: true });
1264
1422
  this.autoFocus = true;
1265
1423
  this.time = 50;
1266
- this.document = inject(DOCUMENT);
1424
+ this.document = inject(DOCUMENT$1);
1267
1425
  this.platformID = inject(PLATFORM_ID);
1268
1426
  }
1269
1427
  ngAfterViewInit() {
@@ -1309,7 +1467,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
1309
1467
  class AXHotkeysService {
1310
1468
  constructor() {
1311
1469
  this.eventManager = inject(EventManager);
1312
- this.document = inject(DOCUMENT);
1470
+ this.document = inject(DOCUMENT$1);
1313
1471
  this.defaults = {
1314
1472
  element: this.document,
1315
1473
  };
@@ -2491,7 +2649,7 @@ class AXRippleDirective {
2491
2649
  this.cdr = inject(ChangeDetectorRef);
2492
2650
  this.enabled = true;
2493
2651
  this.axRippleColor = 'var(--ax-comp-ripple-color, rgba(255, 255, 255, 0.3))';
2494
- this.document = inject(DOCUMENT);
2652
+ this.document = inject(DOCUMENT$1);
2495
2653
  this.platformID = inject(PLATFORM_ID);
2496
2654
  }
2497
2655
  ngOnInit() {