@elderbyte/ngx-starter 21.11.0-beta.0 → 21.12.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.
@@ -6,8 +6,8 @@ import * as i1 from '@angular/platform-browser';
6
6
  import { DomSanitizer } from '@angular/platform-browser';
7
7
  import { Duration, Period, TemporalQueries, LocalTime, Instant, LocalDate, nativeJs, ZoneId, DateTimeFormatter, convert, ZonedDateTime, Temporal as Temporal$1 } from '@js-joda/core';
8
8
  import { LoggerFactory } from '@elderbyte/ts-logger';
9
- import { timer, defer, ReplaySubject, concat, finalize, exhaustMap, BehaviorSubject, Subject, switchMap, of, combineLatest, EMPTY, merge, throwError, forkJoin, mergeWith, Observable, from, toArray, zip, mergeMap as mergeMap$1, fromEvent, mergeAll, skipUntil, filter as filter$1, map as map$1, combineLatestWith as combineLatestWith$1, tap as tap$1, takeUntil as takeUntil$1, NEVER } from 'rxjs';
10
- import { tap, takeUntil, takeWhile, map, filter, distinctUntilChanged, debounceTime, catchError, first, take, switchMap as switchMap$1, mergeMap, expand, reduce, combineLatestWith, startWith, skip, delay, share, skipWhile, debounce, timeout } from 'rxjs/operators';
9
+ import { timer, defer, ReplaySubject, concat, finalize, exhaustMap, BehaviorSubject, Subject, of, throwError, switchMap, combineLatest, EMPTY, merge, forkJoin, mergeWith, Observable, from, toArray, zip, mergeMap as mergeMap$1, fromEvent, mergeAll, skipUntil, filter as filter$1, map as map$1, combineLatestWith as combineLatestWith$1, tap as tap$1, takeUntil as takeUntil$1, NEVER } from 'rxjs';
10
+ import { tap, takeUntil, takeWhile, map, filter, distinctUntilChanged, debounceTime, catchError, first, take, switchMap as switchMap$1, mergeMap, expand, reduce, combineLatestWith, startWith, skip, delay, share, debounce, timeout, skipWhile } from 'rxjs/operators';
11
11
  import { Temporal } from '@js-temporal/polyfill';
12
12
  import * as i1$1 from '@angular/common/http';
13
13
  import { HttpParams, HttpEventType, HttpRequest, HttpClient, HttpErrorResponse, HTTP_INTERCEPTORS, HttpBackend } from '@angular/common/http';
@@ -4082,1814 +4082,1806 @@ class SortContext {
4082
4082
  }
4083
4083
  }
4084
4084
 
4085
- /***************************************************************************
4086
- * *
4087
- * Type Predicates *
4088
- * *
4089
- **************************************************************************/
4090
- function isDataSource(object) {
4091
- if (!object) {
4092
- return false;
4085
+ class DataSourceEntityPatch {
4086
+ constructor(entityId, patch) {
4087
+ this.entityId = entityId;
4088
+ this.patch = patch;
4093
4089
  }
4094
- return (object.dataChanged !== undefined &&
4095
- object.findById !== undefined &&
4096
- object.getId !== undefined);
4097
- }
4098
- function isListDataSource(object) {
4099
- return object.findAllFiltered !== undefined;
4100
- }
4101
- function isLocalListDataSource(object) {
4102
- return (isListDataSource(object) &&
4103
- object.delete !== undefined &&
4104
- object.save !== undefined &&
4105
- object.replaceAll !== undefined);
4106
4090
  }
4107
- function isLocalPagedDataSource(object) {
4108
- return (isPagedDataSource(object) &&
4109
- object.findAllPaged !== undefined &&
4110
- object.findById !== undefined &&
4111
- object.findByIds !== undefined);
4112
- }
4113
- function isLocalDataSource(object) {
4114
- if (!object) {
4115
- return false;
4091
+ /**
4092
+ * Notifies about changes in a DataSource.
4093
+ */
4094
+ class DataSourceChangeEvent {
4095
+ static unknownChanges() {
4096
+ return new DataSourceChangeEvent(null, null, null, null);
4097
+ }
4098
+ static deleted(ids) {
4099
+ return new DataSourceChangeEvent(ids, null, null, null);
4100
+ }
4101
+ static modified(modified) {
4102
+ return new DataSourceChangeEvent(null, modified, null, null);
4103
+ }
4104
+ static created(created) {
4105
+ return new DataSourceChangeEvent(null, null, created, null);
4106
+ }
4107
+ static patched(patches) {
4108
+ return new DataSourceChangeEvent(null, null, null, patches);
4109
+ }
4110
+ constructor(deletedIds, modified, created, patches) {
4111
+ this.deletedIds = deletedIds;
4112
+ this.modified = modified;
4113
+ this.created = created;
4114
+ this.patches = patches;
4115
+ this.unknownChanges = !deletedIds && !modified && !created && !patches;
4116
4116
  }
4117
- return isLocalListDataSource(object) || isLocalPagedDataSource(object);
4118
- }
4119
- function isPagedDataSource(object) {
4120
- return object.findAllPaged !== undefined;
4121
- }
4122
- function isContinuableDataSource(object) {
4123
- return object.findAllContinuable !== undefined;
4124
4117
  }
4125
4118
 
4126
- class ReloadRequest {
4127
- constructor(number, reason) {
4128
- this.number = number;
4129
- this.reason = reason;
4119
+ class EntityIdUtil {
4120
+ static getId(entity, idProperty) {
4121
+ if (entity && idProperty) {
4122
+ if (typeof entity === 'object') {
4123
+ return entity?.[idProperty];
4124
+ }
4125
+ }
4126
+ return entity;
4130
4127
  }
4131
- }
4132
- class AfterReload {
4133
- constructor(request, result) {
4134
- this.request = request;
4135
- this.result = result;
4128
+ static extractIdFnOrProperty(idPropertyOrExtractFn) {
4129
+ if (typeof idPropertyOrExtractFn === 'string') {
4130
+ return EntityIdUtil.extractIdFn(idPropertyOrExtractFn);
4131
+ }
4132
+ else if (typeof idPropertyOrExtractFn === 'function') {
4133
+ return idPropertyOrExtractFn;
4134
+ }
4135
+ else if (idPropertyOrExtractFn === null || idPropertyOrExtractFn === undefined) {
4136
+ return EntityIdUtil.extractIdFn(null);
4137
+ }
4138
+ else {
4139
+ throw new Error('Invalid idPropertyOrExtractFn');
4140
+ }
4136
4141
  }
4137
- fulfills(minRequest) {
4138
- return this.request.number >= minRequest.number;
4142
+ static extractIdFn(idProperty) {
4143
+ return (entity) => EntityIdUtil.getId(entity, idProperty);
4139
4144
  }
4140
4145
  }
4141
- class DataContextBase extends DataSource {
4142
- /***************************************************************************
4143
- * *
4144
- * Fields *
4145
- * *
4146
- **************************************************************************/
4147
- static { this.DC_ID_COUNTER = 0; }
4146
+
4147
+ class DataSourceBase {
4148
4148
  /***************************************************************************
4149
4149
  * *
4150
4150
  * Constructor *
4151
4151
  * *
4152
4152
  **************************************************************************/
4153
- constructor(dataSource, _indexFn, _localApply, _localSort) {
4154
- super();
4155
- this._indexFn = _indexFn;
4156
- this._localApply = _localApply;
4157
- this._localSort = _localSort;
4158
- this.baselog = LoggerFactory.getLogger('DataContextBase');
4159
- this._filter = new FilterContext();
4160
- this._sort = new SortContext();
4161
- this._total = new BehaviorSubject(undefined);
4162
- this._status = new BehaviorSubject(DataContextStatus.idle());
4163
- this._started = new BehaviorSubject(false);
4164
- this._closed = new BehaviorSubject(false);
4165
- this._customIndex = new Map();
4166
- this._reloadCounter = 0;
4167
- this._reloadQueue = new Subject();
4168
- this._reloaded = new Subject();
4169
- this.destroy$ = new Subject();
4170
- this.id = 'DataContext#' + ++DataContextBase.DC_ID_COUNTER;
4171
- this._dataSource = dataSource;
4172
- this._data = new IndexedEntities((e) => dataSource.getId(e), _localSort, this._sort);
4173
- this._loading = this._status.pipe(map((status) => status.loading));
4174
- this._filter.filters
4175
- .pipe(filter(() => this.started), takeUntil(this.destroy$))
4176
- .subscribe((filters) => this.onFiltersChanged(filters));
4177
- this._sort.sorts
4178
- .pipe(filter(() => this.started), takeUntil(this.destroy$))
4179
- .subscribe((sorts) => this.onSortsChanged(sorts));
4180
- this._reloadQueue
4181
- .pipe(takeUntil(this.destroy$), filter((request) => this.started), debounceTime(this.getDebounceTime()), switchMap((request) => this.reloadNow(request).pipe(map((result) => new AfterReload(request, result)), catchError((err) => {
4182
- // Dont die on errors
4183
- this.baselog.error(this.id + ': Reload queue detected error, bad!', err);
4184
- return of(new AfterReload(request, err));
4185
- }))))
4186
- .subscribe((afterReload) => this._reloaded.next(afterReload));
4153
+ constructor(propertyOrIdExtractor) {
4154
+ /***************************************************************************
4155
+ * *
4156
+ * Fields *
4157
+ * *
4158
+ **************************************************************************/
4159
+ this.dataChangeEvents$ = new Subject();
4160
+ this.extractIdFn = EntityIdUtil.extractIdFnOrProperty(propertyOrIdExtractor);
4187
4161
  }
4188
4162
  /***************************************************************************
4189
4163
  * *
4190
- * Public API Material DC *
4164
+ * Properties *
4191
4165
  * *
4192
4166
  **************************************************************************/
4193
- connect(collectionViewer) {
4194
- return this.data;
4167
+ get dataChanged() {
4168
+ return this.dataChangeEvents$.asObservable();
4195
4169
  }
4196
- disconnect(collectionViewer) { }
4197
4170
  /***************************************************************************
4198
4171
  * *
4199
- * Properties *
4172
+ * Public API *
4200
4173
  * *
4201
4174
  **************************************************************************/
4202
- get dataSource() {
4203
- return this._dataSource;
4204
- }
4205
- get snapshot() {
4206
- return new DataContextSnapshot(this.dataSnapshot, this.isEmpty, this.loadingSnapshot, this.totalSnapshot, this.statusSnapshot);
4207
- }
4208
- get total() {
4209
- return this._total.asObservable();
4210
- }
4211
- get totalSnapshot() {
4212
- return this._total.getValue();
4175
+ publishChangeEvent(e) {
4176
+ this.dataChangeEvents$.next(e);
4213
4177
  }
4214
- get sort() {
4215
- return this._sort;
4178
+ getId(entity) {
4179
+ return this.extractIdFn(entity);
4216
4180
  }
4217
- get filter() {
4218
- return this._filter;
4181
+ }
4182
+
4183
+ class SortUtil {
4184
+ static toggleDir(dir) {
4185
+ if (dir === 'asc') {
4186
+ return 'desc';
4187
+ }
4188
+ else {
4189
+ return 'asc';
4190
+ }
4219
4191
  }
4220
- get loading() {
4221
- return this._loading;
4192
+ static toggleSort(sort) {
4193
+ return new Sort(sort.prop, SortUtil.toggleDir(sort.dir));
4222
4194
  }
4223
- get loadingSnapshot() {
4224
- return this.statusSnapshot?.loading;
4195
+ static sortData(data, sorts, prefix) {
4196
+ if (sorts && sorts.length > 0) {
4197
+ const copy = [...data];
4198
+ const sortFields = sorts.map((s) => (s.dir === 'desc' ? '-' : '') + SortUtil.propertyPath(s, prefix));
4199
+ return copy.sort(ComparatorBuilder.fieldSort(...sortFields));
4200
+ }
4201
+ else {
4202
+ return data;
4203
+ }
4225
4204
  }
4226
- get data() {
4227
- return this._data.entities$;
4205
+ /**
4206
+ * Checks if the two arrays have all content references in the exact same order.
4207
+ * @param data
4208
+ * @param other
4209
+ */
4210
+ static equalsExactRefs(data, other) {
4211
+ // eslint-disable-next-line eqeqeq
4212
+ if (data.length == other.length) {
4213
+ for (let i = 0; i < data.length; i++) {
4214
+ // eslint-disable-next-line eqeqeq
4215
+ if (data[i] != other[i]) {
4216
+ return false;
4217
+ }
4218
+ }
4219
+ return true;
4220
+ }
4221
+ return false;
4228
4222
  }
4229
- get dataSnapshot() {
4230
- return this._data.entitiesSnapshot;
4223
+ static propertyPath(sort, prefix) {
4224
+ if (prefix) {
4225
+ return prefix + '.' + sort.prop;
4226
+ }
4227
+ else {
4228
+ return sort.prop;
4229
+ }
4231
4230
  }
4232
- get status() {
4233
- return this._status;
4231
+ }
4232
+
4233
+ class MapUtils {
4234
+ static toDistinctMap(values, keyFn) {
4235
+ return values.reduce((map, value) => {
4236
+ map.set(keyFn(value), value);
4237
+ return map;
4238
+ }, new Map());
4234
4239
  }
4235
- get statusSnapshot() {
4236
- return this._status.getValue();
4240
+ static groupByKey(values, keyFn) {
4241
+ const groups = new Map();
4242
+ values.forEach((value) => {
4243
+ const key = keyFn(value);
4244
+ let group = groups.get(key);
4245
+ if (!group) {
4246
+ group = [];
4247
+ groups.set(key, group);
4248
+ }
4249
+ group.push(value);
4250
+ });
4251
+ return groups;
4237
4252
  }
4238
- get isEmpty() {
4239
- return this.dataSnapshot.length === 0;
4240
- }
4241
- get isStarted() {
4242
- return this.started;
4243
- }
4244
- get isStarted$() {
4245
- return this._started.asObservable();
4246
- }
4247
- get isClosed() {
4248
- return this._closed.getValue();
4249
- }
4250
- get started() {
4251
- return this._started.getValue();
4252
- }
4253
- set started(started) {
4254
- this._started.next(started);
4255
- }
4256
- /***************************************************************************
4257
- * *
4258
- * Public API *
4259
- * *
4260
- **************************************************************************/
4261
- start(sorts, filters) {
4262
- if (this.isClosed) {
4263
- throw new Error(this.id + ': Restarting a closed DataContext is not possible!'); // See unsubscribe$
4264
- }
4265
- this.started = false;
4266
- this._sort.replaceSorts(sorts || this.sort.sortsSnapshot);
4267
- this._filter.replaceFilters(filters || this.filter.filtersSnapshot);
4268
- this.baselog.debug(this.id + ': Start ...', {
4269
- dataSource: this.dataSource,
4270
- sort: this._sort.sortsSnapshot,
4271
- filter: this._filter.filtersSnapshot,
4272
- });
4273
- this.started = true;
4274
- return this.reload('START');
4275
- }
4276
- reload(reason) {
4277
- const request = new ReloadRequest(++this._reloadCounter, reason);
4278
- this.baselog.debug(this.id + ': Enqueuing reload request #' + request.number + ' [' + reason + ']');
4279
- this._reloadQueue.next(request);
4280
- return this._reloaded.pipe(filter((reload) => reload.fulfills(request)), map((reload) => reload.result));
4281
- }
4282
- findByIndex(key) {
4283
- if (!this._indexFn) {
4284
- throw new Error(this.id + ': findByIndex requires you to pass a index function!');
4285
- }
4286
- return this._customIndex.get(key);
4287
- }
4288
- findById(id) {
4289
- return this._data.getById(id);
4290
- }
4291
- /**
4292
- * Closes this DataContext and cleans up all used resources.
4293
- */
4294
- close() {
4295
- this.started = false;
4296
- this.destroy$.next();
4297
- this.destroy$.complete();
4298
- this._closed.next(true);
4299
- this._data.destroy();
4300
- this._total.complete();
4301
- this._status.complete();
4302
- this.clearAll();
4303
- this.baselog.debug(this.id + ': Has been closed and resources cleaned up!');
4304
- }
4305
- refresh() {
4306
- this._data.notify();
4307
- }
4308
- update(entities) {
4309
- this._data.updateSome(entities);
4253
+ static mapValue(map, valueMapFn) {
4254
+ const newMap = new Map();
4255
+ Array.from(map.entries()).forEach(([key, value]) => newMap.set(key, valueMapFn(value)));
4256
+ return newMap;
4310
4257
  }
4258
+ }
4259
+
4260
+ class LocalListDataSource extends DataSourceBase {
4311
4261
  /***************************************************************************
4312
4262
  * *
4313
- * Protected methods *
4263
+ * Static Builder *
4314
4264
  * *
4315
4265
  **************************************************************************/
4316
- reloadNow(request) {
4317
- this.baselog.debug(this.id + ': reloadNow triggered. Request #' + request.number + ' ' + request.reason, {
4318
- filter: this._filter.filtersSnapshot,
4319
- sort: this._sort.sortsSnapshot,
4320
- dataSource: this.dataSource,
4321
- request: request,
4322
- });
4323
- return this.reloadInternal(); // TODO This should actually return the real (http) request
4324
- }
4325
- /**
4326
- * Append the given rows to the existing ones.
4327
- */
4328
- appendData(additionalData) {
4329
- additionalData = this.localApply(additionalData);
4330
- this.indexAll(additionalData);
4331
- this._data.append(additionalData);
4332
- }
4333
- /**
4334
- * Insert the given rows at the given location.
4335
- */
4336
- insertData(additionalData, offset) {
4337
- additionalData = this.localApply(additionalData);
4338
- this.indexAll(additionalData);
4339
- this._data.insert(additionalData, offset);
4340
- }
4341
- /**
4342
- * Replaces the existing data with the new set.
4343
- * @param newData The new data set.
4344
- * @param alreadyIndexed The rows will be indexed unless they already have been.
4345
- */
4346
- setData(newData, alreadyIndexed = false) {
4347
- newData = this.localApply(newData);
4348
- if (!alreadyIndexed) {
4349
- this.clearIndex();
4350
- this.indexAll(newData);
4351
- }
4352
- this._data.replaceAll(newData);
4353
- }
4354
- clearData() {
4355
- this.setData([]);
4356
- }
4357
- localApply(data) {
4358
- if (this._localApply) {
4359
- return this._localApply(data);
4360
- }
4361
- else {
4362
- return data;
4363
- }
4364
- }
4365
- setTotal(total) {
4366
- this._total.next(total);
4367
- }
4368
4266
  /**
4369
- * Clears the current data-context cached data.
4370
- * State such as current sorting and filtersSnapshot are kept.
4371
- */
4372
- clearAll(silent = false) {
4373
- this.clearIndex();
4374
- if (!silent) {
4375
- this.setTotal(0);
4376
- this.clearData();
4377
- }
4378
- this.onIdle();
4379
- }
4380
- updateIndex() {
4381
- this.clearIndex();
4382
- this.indexAll(this.dataSnapshot);
4383
- }
4384
- /**
4385
- * Indexes the given item by key if there is an index function defined.
4386
- */
4387
- indexItem(item) {
4388
- const key = this.getItemKey(item);
4389
- if (key) {
4390
- this._customIndex.set(key, item);
4391
- }
4392
- }
4393
- /**
4394
- * Indexes all the given items by key if there is an index function defined.
4267
+ * Creates an empty local list data-source.
4268
+ * You can set / modify data by using the data property.
4269
+ * @param idPropertyOrExtractor
4270
+ * @param localSort
4271
+ * @param localFilter
4395
4272
  */
4396
- indexAll(items) {
4397
- if (this._indexFn) {
4398
- items.forEach((item) => this.indexItem(item));
4399
- }
4400
- }
4401
- getItemKey(item) {
4402
- if (this._indexFn) {
4403
- return this._indexFn(item);
4404
- }
4405
- return null;
4406
- }
4407
- getItemId(item) {
4408
- return this.dataSource.getId(item);
4409
- }
4410
- getDebounceTime() {
4411
- if (isLocalDataSource(this.dataSource)) {
4412
- return 0;
4413
- }
4414
- return 50; // Default debounce time
4273
+ static empty(idPropertyOrExtractor, localSort, localFilter) {
4274
+ return this.from([], idPropertyOrExtractor, localSort, localFilter);
4415
4275
  }
4416
- /***************************************************************************
4417
- * *
4418
- * Event handler *
4419
- * *
4420
- **************************************************************************/
4421
- /**
4422
- * Occurs when the sort has changed.
4423
- */
4424
- onSortsChanged(sorts) {
4425
- if (!this._localSort) {
4426
- this.reload('Sort Changed');
4427
- }
4428
- else {
4429
- this.setData(this.dataSnapshot, true);
4276
+ static from(localData, idPropertyOrExtractor, localSort, localFilter) {
4277
+ if (idPropertyOrExtractor === null) {
4278
+ idPropertyOrExtractor = LocalListDataSource.guessIdProperty(localData);
4430
4279
  }
4280
+ return new LocalListDataSource(localData, localSort, localFilter, idPropertyOrExtractor);
4431
4281
  }
4432
- /**
4433
- * Occurs when the filter has changed.
4434
- */
4435
- onFiltersChanged(filters) {
4436
- this.reload('Filters Changed');
4437
- }
4438
- onError(err) {
4439
- this.onStatus(DataContextStatus.error(err));
4440
- }
4441
- onIdle() {
4442
- this.onStatus(DataContextStatus.idle());
4443
- }
4444
- onLoading() {
4445
- this.onStatus(DataContextStatus.loading());
4446
- }
4447
- onStatus(status) {
4448
- this._status.next(status);
4449
- }
4450
- /***************************************************************************
4451
- * *
4452
- * Private methods *
4453
- * *
4454
- **************************************************************************/
4455
- clearIndex() {
4456
- this._customIndex.clear();
4457
- }
4458
- }
4459
-
4460
- class DataContextSimple extends DataContextBase {
4461
4282
  /***************************************************************************
4462
4283
  * *
4463
4284
  * Constructor *
4464
4285
  * *
4465
4286
  **************************************************************************/
4466
- constructor(dataSource, indexFn, localApply, localSort) {
4467
- super(dataSource, indexFn, localApply, localSort);
4287
+ constructor(localData, localSort, localFilter, idPropertyOrExtractor) {
4288
+ super(idPropertyOrExtractor);
4468
4289
  /***************************************************************************
4469
4290
  * *
4470
4291
  * Fields *
4471
4292
  * *
4472
4293
  **************************************************************************/
4473
- this.log = LoggerFactory.getLogger(this.constructor.name);
4294
+ this.logger = LoggerFactory.getLogger(this.constructor.name);
4295
+ this.data$ = new BehaviorSubject([]);
4296
+ if (!localData) {
4297
+ throw new Error('localData must not be null!');
4298
+ }
4299
+ this.localSort = localSort || SortUtil.sortData;
4300
+ this.localFilter = localFilter || FilterUtil.filterData;
4301
+ this.data = localData;
4474
4302
  }
4475
4303
  /***************************************************************************
4476
4304
  * *
4477
4305
  * Properties *
4478
4306
  * *
4479
4307
  **************************************************************************/
4480
- get dataSource() {
4481
- return super.dataSource;
4308
+ get data() {
4309
+ return this.data$.getValue();
4310
+ }
4311
+ set data(data) {
4312
+ this.replaceAll(data);
4482
4313
  }
4483
4314
  /***************************************************************************
4484
4315
  * *
4485
- * Private methods *
4316
+ * IDataSource API *
4486
4317
  * *
4487
4318
  **************************************************************************/
4488
- reloadInternal() {
4489
- const subject = new Subject();
4490
- if (this.dataSource) {
4491
- this.onLoading();
4492
- this.dataSource
4493
- .findAllFiltered(this.filter.filtersSnapshot, this.sort.sortsSnapshot)
4494
- .pipe(first())
4495
- .subscribe((list) => {
4496
- this.onIdle();
4497
- this.setTotal(list.length);
4498
- this.setData(list);
4499
- this.log.debug(this.id + ': Got list data: ' + list.length);
4500
- subject.next();
4501
- }, (err) => {
4502
- this.onError(err);
4503
- this.clearAll();
4504
- this.log.error(this.id + ': Failed to query data', err);
4505
- subject.error(err);
4506
- });
4319
+ findById(id) {
4320
+ if (id === undefined || id === null) {
4321
+ throw new Error('findById: id argument required!');
4322
+ }
4323
+ const found = this.data.find((d) => this.getId(d) === id);
4324
+ if (found) {
4325
+ return of(found);
4507
4326
  }
4508
4327
  else {
4509
- const errorMsg = this.id + ': Skipping data context load - no list fetcher present!';
4510
- this.log.warn(errorMsg);
4511
- subject.error(new Error(errorMsg));
4328
+ return throwError(() => new Error("Could not find local entity by id: '" + id + "'"));
4512
4329
  }
4513
- return subject.pipe(first());
4514
4330
  }
4515
- }
4516
-
4517
- /**
4518
- * Extends a simple flat list data-context with infinite-scroll pagination support.
4519
- *
4520
- */
4521
- class DataContextContinuableBase extends DataContextBase {
4522
- /***************************************************************************
4523
- * *
4524
- * Constructors *
4525
- * *
4526
- **************************************************************************/
4527
- constructor(dataSource, chunkSize, indexFn, localApply, localSort) {
4528
- super(dataSource, indexFn, localApply, localSort);
4529
- /***************************************************************************
4530
- * *
4531
- * Fields *
4532
- * *
4533
- **************************************************************************/
4534
- this.cblogger = LoggerFactory.getLogger(this.constructor.name);
4535
- this._chunkSize$ = new BehaviorSubject(chunkSize);
4331
+ findByIds(ids) {
4332
+ if (ids === undefined || ids === null) {
4333
+ throw new Error('findByIds: ids array argument required!');
4334
+ }
4335
+ const desiredIds = new Set(ids);
4336
+ return of(this.data.filter((d) => desiredIds.has(this.getId(d))));
4337
+ }
4338
+ findAllFiltered(filters, sorts) {
4339
+ return of(this.data).pipe(map((data) => this.localFilter(data, filters)), map((data) => this.localSort(data, sorts)));
4536
4340
  }
4537
4341
  /***************************************************************************
4538
4342
  * *
4539
4343
  * Public API *
4540
4344
  * *
4541
4345
  **************************************************************************/
4542
- loadAll(sorts, filters) {
4543
- this.cblogger.debug(this.id + ': Starting to load all data ...');
4544
- // load first page
4545
- this.start(sorts, filters).subscribe({
4546
- next: () => {
4547
- this.cblogger.debug(this.id + ': First page has been loaded. Loading remaining data ...');
4548
- // load rest in a recursive manner
4549
- this.loadAllRec();
4550
- },
4551
- error: (err) => {
4552
- this.onError(err);
4553
- this.cblogger.error(this.id + ': Failed to load first page of load all procedure!', err);
4554
- },
4555
- });
4346
+ replaceAll(data) {
4347
+ this.silentReplaceData(data);
4348
+ this.publishChangeEvent(DataSourceChangeEvent.unknownChanges());
4556
4349
  }
4557
- get chunkSize$() {
4558
- return this._chunkSize$.asObservable();
4350
+ delete(entity) {
4351
+ this.deleteAll([entity]);
4559
4352
  }
4560
- get chunkSize() {
4561
- return this._chunkSize$.value;
4353
+ deleteAll(toDelete) {
4354
+ if (toDelete?.length > 0) {
4355
+ return this.deleteAllById(toDelete.map((e) => this.getId(e)));
4356
+ }
4562
4357
  }
4563
- set chunkSize(size) {
4564
- this.updateChunkSize(size, true);
4358
+ deleteAllById(idsToDelete) {
4359
+ if (idsToDelete?.length > 0) {
4360
+ const existing = this.data;
4361
+ const idsToDeleteSet = new Set(idsToDelete);
4362
+ this.silentReplaceData(existing.filter((e) => !idsToDeleteSet.has(this.getId(e))));
4363
+ this.publishChangeEvent(DataSourceChangeEvent.deleted(idsToDelete));
4364
+ }
4365
+ }
4366
+ saveAll(toSave) {
4367
+ const idDataMap = this.buildIdDataMap(this.data);
4368
+ const createdEntities = [];
4369
+ const modifiedEntities = [];
4370
+ toSave.forEach((entity) => {
4371
+ const id = this.getId(entity);
4372
+ if (!idDataMap.has(id)) {
4373
+ createdEntities.push(entity);
4374
+ }
4375
+ else {
4376
+ modifiedEntities.push(entity);
4377
+ }
4378
+ idDataMap.set(id, entity);
4379
+ });
4380
+ this.silentReplaceData(Array.from(idDataMap.values()));
4381
+ this.publishChanges(createdEntities, modifiedEntities);
4382
+ }
4383
+ save(entity, index) {
4384
+ this.saveAtIndex(entity, index);
4385
+ }
4386
+ saveAtIndex(entity, index) {
4387
+ const id = this.getId(entity);
4388
+ const newData = [...this.data];
4389
+ const existingIndex = newData.findIndex((entity) => this.getId(entity) === id);
4390
+ let created = false;
4391
+ if (existingIndex === -1) {
4392
+ if (index !== null) {
4393
+ newData.splice(index, 0, entity);
4394
+ }
4395
+ else {
4396
+ newData.push(entity);
4397
+ }
4398
+ created = true;
4399
+ }
4400
+ else {
4401
+ newData[existingIndex] = entity;
4402
+ }
4403
+ this.silentReplaceData(newData);
4404
+ this.publishChangeEvent(created ? DataSourceChangeEvent.created([entity]) : DataSourceChangeEvent.modified([entity]));
4565
4405
  }
4566
4406
  /***************************************************************************
4567
4407
  * *
4568
- * Private Methods *
4408
+ * Private methods *
4569
4409
  * *
4570
4410
  **************************************************************************/
4571
- updateChunkSize(newSize, reloadOnChange) {
4572
- if (this._chunkSize$.value !== newSize) {
4573
- this._chunkSize$.next(newSize);
4574
- if (reloadOnChange) {
4575
- this.reload('ChunkSizeChanged:' + newSize);
4411
+ buildIdDataMap(data) {
4412
+ return MapUtils.toDistinctMap(data, (value) => this.getId(value));
4413
+ }
4414
+ silentReplaceData(newData) {
4415
+ this.data$.next(newData);
4416
+ }
4417
+ static guessIdProperty(localData) {
4418
+ const log = LoggerFactory.getLogger('LocalListDataSource');
4419
+ if (localData && localData.length > 0) {
4420
+ const sample = localData[0];
4421
+ if (typeof sample === 'object') {
4422
+ if (Object.prototype.hasOwnProperty.call(sample, 'id')) {
4423
+ log.warn('DataSource without defined id-property => autodetected property id-property as "id"');
4424
+ return 'id'; // Use id
4425
+ }
4426
+ else {
4427
+ log.warn('Local DataSource created without defined id-property and objects. Using object equality!');
4428
+ }
4576
4429
  }
4577
4430
  }
4431
+ return null; // Use value as id if scalar
4578
4432
  }
4579
- loadAllRec() {
4580
- this.loadMore().subscribe({
4581
- next: () => {
4582
- this.cblogger.debug(this.id + ': Loading data chunk finished, loading next...');
4583
- this.loadAllRec();
4584
- },
4585
- error: (err) => {
4586
- this.onError(err);
4587
- this.cblogger.error(this.id + ': Loading all failed!', err);
4588
- },
4589
- complete: () => {
4590
- this.cblogger.info(this.id + ': All data loaded completely.');
4591
- },
4592
- });
4433
+ publishChanges(createdEntities, modifiedEntities) {
4434
+ if (createdEntities.length > 0) {
4435
+ this.publishChangeEvent(DataSourceChangeEvent.created(createdEntities));
4436
+ }
4437
+ if (modifiedEntities.length > 0) {
4438
+ this.publishChangeEvent(DataSourceChangeEvent.modified(modifiedEntities));
4439
+ }
4593
4440
  }
4594
4441
  }
4595
4442
 
4596
- /**
4597
- * Extends a simple flat list data-context with infinite-scroll pagination support.
4598
- *
4599
- */
4600
- class DataContextContinuablePaged extends DataContextContinuableBase {
4601
- /***************************************************************************
4602
- * *
4603
- * Constructors *
4604
- * *
4605
- **************************************************************************/
4606
- constructor(dataSource, pageSize, indexFn, localApply, localSort) {
4607
- super(dataSource, pageSize, indexFn, localApply, localSort);
4608
- /***************************************************************************
4609
- * *
4610
- * Fields *
4611
- * *
4612
- **************************************************************************/
4613
- this.logger = LoggerFactory.getLogger(this.constructor.name);
4614
- this._pageCache = new Map();
4615
- this._latestPage = 0;
4616
- this._hasMoreData = combineLatest([this.total, this.data]).pipe(map(([total, data]) => this.checkHasMoreData(total, data)), takeUntil(this.destroy$));
4617
- }
4618
- /***************************************************************************
4619
- * *
4620
- * Properties *
4621
- * *
4622
- **************************************************************************/
4623
- get dataSource() {
4624
- return super.dataSource;
4625
- }
4443
+ class LocalPagedDataSource {
4626
4444
  /***************************************************************************
4627
4445
  * *
4628
- * Public API *
4446
+ * Static Builder *
4629
4447
  * *
4630
4448
  **************************************************************************/
4631
4449
  /**
4632
- * Load the next chunk of data.
4633
- * Useful for infinite scroll like data flows.
4634
- *
4450
+ * Creates an empty local list data-source.
4451
+ * You can set / modify data by using the data property.
4452
+ * @param idProperty
4453
+ * @param localSort
4454
+ * @param localFilter
4635
4455
  */
4636
- loadMore() {
4637
- if (this.hasMoreDataSnapshot) {
4638
- this.logger.info(this.id + ': Loading more...' + this._latestPage);
4639
- if (this.loadingSnapshot) {
4640
- return EMPTY;
4641
- }
4642
- const nextPage = this._latestPage + 1;
4643
- return this.fetchPage(nextPage, this.chunkSize);
4644
- }
4645
- else {
4646
- this.logger.debug(this.id + ': Cannot load more data, since no more data available.');
4647
- return EMPTY;
4648
- }
4649
- }
4650
- get hasMoreData() {
4651
- return this._hasMoreData;
4456
+ static empty(idProperty, localSort, localFilter) {
4457
+ return LocalPagedDataSource.of(LocalListDataSource.empty(idProperty, localSort, localFilter));
4652
4458
  }
4653
- get hasMoreDataSnapshot() {
4654
- return this.checkHasMoreData(this.totalSnapshot, this.dataSnapshot);
4459
+ static of(listDataSource) {
4460
+ return new LocalPagedDataSource(listDataSource);
4655
4461
  }
4656
4462
  /***************************************************************************
4657
4463
  * *
4658
- * Private Methods *
4464
+ * Constructor *
4659
4465
  * *
4660
4466
  **************************************************************************/
4661
- checkHasMoreData(total, data) {
4662
- if (total) {
4663
- return total > data.length;
4467
+ constructor(listDataSource) {
4468
+ if (!listDataSource) {
4469
+ throw new Error('listDataSource must not be null!');
4664
4470
  }
4665
- return false;
4471
+ this.localListFetcher = listDataSource;
4666
4472
  }
4667
- reloadInternal() {
4668
- // Since continuable data-contexts are appending data,
4669
- // we need to clear it for a reload.
4670
- this.clearAll(true);
4671
- return this.fetchPage(0, this.chunkSize, true);
4473
+ /***************************************************************************
4474
+ * *
4475
+ * Public API *
4476
+ * *
4477
+ **************************************************************************/
4478
+ get dataChanged() {
4479
+ return this.localListFetcher.dataChanged;
4672
4480
  }
4673
- clearAll(silent = false) {
4674
- super.clearAll(silent);
4675
- this._pageCache = new Map();
4676
- this._latestPage = 0;
4481
+ findById(id) {
4482
+ return this.localListFetcher.findById(id);
4677
4483
  }
4678
- fetchPage(pageIndex, pageSize, clear = false) {
4679
- const subject = new Subject();
4680
- const pageRequest = new Pageable(pageIndex, pageSize, this.sort.sortsSnapshot);
4681
- if (this._pageCache.has(pageIndex)) {
4682
- // Page already loaded - skipping request!
4683
- this.logger.debug(this.id + ': Skipping fetching page since its already in page observable cache.');
4684
- subject.next();
4484
+ findByIds(ids) {
4485
+ return this.localListFetcher.findByIds(ids);
4486
+ }
4487
+ findAllPaged(pageable, filters) {
4488
+ return this.localListFetcher
4489
+ .findAllFiltered(filters, pageable.sorts)
4490
+ .pipe(map((data) => this.pageSlice(pageable, data)));
4491
+ }
4492
+ getId(entity) {
4493
+ return this.localListFetcher.getId(entity);
4494
+ }
4495
+ /***************************************************************************
4496
+ * *
4497
+ * Private methods *
4498
+ * *
4499
+ **************************************************************************/
4500
+ pageSlice(pageable, data) {
4501
+ let page;
4502
+ if (data) {
4503
+ const start = pageable.page * pageable.size;
4504
+ const end = start + pageable.size;
4505
+ const slice = data.slice(start, end);
4506
+ page = Page.fromPage(slice, data.length, pageable);
4685
4507
  }
4686
4508
  else {
4687
- this.onLoading();
4688
- this.logger.debug(this.id + `: Loading page ${pageIndex} using pageable:`, pageRequest);
4689
- const pageObs = this.dataSource.findAllPaged(pageRequest, this.filter.filtersSnapshot);
4690
- this._pageCache.set(pageIndex, pageObs);
4691
- pageObs.subscribe((page) => {
4692
- this.logger.debug(this.id + ': Got page data:', page);
4693
- this.populatePageData(page, clear);
4694
- if (this._latestPage < page.number) {
4695
- this._latestPage = page.number; // TODO This might cause that pages are skipped
4696
- }
4697
- subject.next();
4698
- this.onIdle();
4699
- }, (err) => {
4700
- this.onError(err);
4701
- this.clearData();
4702
- this.setTotal(0);
4703
- this.logger.error(this.id + ': Failed to query data', err);
4704
- subject.error(err);
4705
- });
4509
+ page = Page.empty();
4706
4510
  }
4707
- return subject.pipe(first());
4511
+ return page;
4708
4512
  }
4709
- /**
4710
- * Load the data from the given page into the current data context
4711
- */
4712
- populatePageData(page, clear) {
4713
- try {
4714
- this.setTotal(page.totalElements);
4715
- const start = page.number * page.size;
4716
- if (clear) {
4717
- this.setData(page.content);
4718
- }
4719
- else {
4720
- this.insertData(page.content, start);
4721
- }
4722
- }
4723
- catch (err) {
4724
- this.onError(err);
4725
- this.logger.error(this.id + ': Failed to populate data with page', page, err);
4726
- }
4513
+ }
4514
+
4515
+ /***************************************************************************
4516
+ * *
4517
+ * Type Predicates *
4518
+ * *
4519
+ **************************************************************************/
4520
+ function isDataSource(object) {
4521
+ if (!object) {
4522
+ return false;
4727
4523
  }
4524
+ return (object.dataChanged !== undefined &&
4525
+ object.findById !== undefined &&
4526
+ object.getId !== undefined);
4527
+ }
4528
+ function isListDataSource(object) {
4529
+ return object.findAllFiltered !== undefined;
4530
+ }
4531
+ function isLocalListDataSource(object) {
4532
+ return object instanceof LocalListDataSource;
4533
+ }
4534
+ function isLocalPagedDataSource(object) {
4535
+ return object instanceof LocalPagedDataSource;
4536
+ }
4537
+ function isLocalDataSource(object) {
4538
+ return isLocalListDataSource(object) || isLocalPagedDataSource(object);
4539
+ }
4540
+ function isPagedDataSource(object) {
4541
+ return object.findAllPaged !== undefined;
4542
+ }
4543
+ function isContinuableDataSource(object) {
4544
+ return object.findAllContinuable !== undefined;
4728
4545
  }
4729
4546
 
4730
- class DataContextContinuableToken extends DataContextContinuableBase {
4547
+ class ReloadRequest {
4548
+ constructor(number, reason) {
4549
+ this.number = number;
4550
+ this.reason = reason;
4551
+ }
4552
+ }
4553
+ class AfterReload {
4554
+ constructor(request, result) {
4555
+ this.request = request;
4556
+ this.result = result;
4557
+ }
4558
+ fulfills(minRequest) {
4559
+ return this.request.number >= minRequest.number;
4560
+ }
4561
+ }
4562
+ const DEFAULT_DEBOUNCE_TIME$1 = 50;
4563
+ class DataContextBase extends DataSource {
4564
+ /***************************************************************************
4565
+ * *
4566
+ * Fields *
4567
+ * *
4568
+ **************************************************************************/
4569
+ static { this.DC_ID_COUNTER = 0; }
4731
4570
  /***************************************************************************
4732
4571
  * *
4733
4572
  * Constructor *
4734
4573
  * *
4735
4574
  **************************************************************************/
4736
- constructor(dataSource, chunkSize, indexFn, localApply, localSort) {
4737
- super(dataSource, chunkSize, indexFn, localApply, localSort);
4738
- /***************************************************************************
4739
- * *
4740
- * Fields *
4741
- * *
4742
- **************************************************************************/
4743
- this.logger = LoggerFactory.getLogger(this.constructor.name);
4744
- this._hasMoreData = new BehaviorSubject(false);
4745
- this._chunkCache = new Set();
4575
+ constructor(dataSource, _indexFn, _localApply, _localSort) {
4576
+ super();
4577
+ this._indexFn = _indexFn;
4578
+ this._localApply = _localApply;
4579
+ this._localSort = _localSort;
4580
+ this.baselog = LoggerFactory.getLogger('DataContextBase');
4581
+ this._filter = new FilterContext();
4582
+ this._sort = new SortContext();
4583
+ this._total = new BehaviorSubject(undefined);
4584
+ this._status = new BehaviorSubject(DataContextStatus.idle());
4585
+ this._started = new BehaviorSubject(false);
4586
+ this._closed = new BehaviorSubject(false);
4587
+ this._customIndex = new Map();
4588
+ this._reloadCounter = 0;
4589
+ this._reloadQueue = new Subject();
4590
+ this._reloaded = new Subject();
4591
+ this.destroy$ = new Subject();
4592
+ this.id = 'DataContext#' + ++DataContextBase.DC_ID_COUNTER;
4593
+ this._dataSource = dataSource;
4594
+ this._data = new IndexedEntities((e) => dataSource.getId(e), _localSort, this._sort);
4595
+ this._loading = this._status.pipe(map((status) => status.loading));
4596
+ this._filter.filters
4597
+ .pipe(filter(() => this.started), takeUntil(this.destroy$))
4598
+ .subscribe((filters) => this.onFiltersChanged(filters));
4599
+ this._sort.sorts
4600
+ .pipe(filter(() => this.started), takeUntil(this.destroy$))
4601
+ .subscribe((sorts) => this.onSortsChanged(sorts));
4602
+ this._reloadQueue
4603
+ .pipe(takeUntil(this.destroy$), filter((request) => this.started), debounceTime(this.getDebounceTime()), switchMap((request) => this.reloadNow(request).pipe(map((result) => new AfterReload(request, result)), catchError((err) => {
4604
+ // Dont die on errors
4605
+ this.baselog.error(this.id + ': Reload queue detected error, bad!', err);
4606
+ return of(new AfterReload(request, err));
4607
+ }))))
4608
+ .subscribe((afterReload) => this._reloaded.next(afterReload));
4746
4609
  }
4747
4610
  /***************************************************************************
4748
4611
  * *
4749
- * Properties *
4612
+ * Public API Material DC *
4750
4613
  * *
4751
4614
  **************************************************************************/
4752
- get dataSource() {
4753
- return super.dataSource;
4615
+ connect(collectionViewer) {
4616
+ return this.data;
4754
4617
  }
4618
+ disconnect(collectionViewer) { }
4755
4619
  /***************************************************************************
4756
4620
  * *
4757
- * Public API *
4621
+ * Properties *
4758
4622
  * *
4759
4623
  **************************************************************************/
4760
- get hasMoreDataSnapshot() {
4761
- return this._hasMoreData.getValue();
4624
+ get dataSource() {
4625
+ return this._dataSource;
4626
+ }
4627
+ get snapshot() {
4628
+ return new DataContextSnapshot(this.dataSnapshot, this.isEmpty, this.loadingSnapshot, this.totalSnapshot, this.statusSnapshot);
4629
+ }
4630
+ get total() {
4631
+ return this._total.asObservable();
4632
+ }
4633
+ get totalSnapshot() {
4634
+ return this._total.getValue();
4635
+ }
4636
+ get sort() {
4637
+ return this._sort;
4638
+ }
4639
+ get filter() {
4640
+ return this._filter;
4641
+ }
4642
+ get loading() {
4643
+ return this._loading;
4644
+ }
4645
+ get loadingSnapshot() {
4646
+ return this.statusSnapshot?.loading;
4647
+ }
4648
+ get data() {
4649
+ return this._data.entities$;
4650
+ }
4651
+ get dataSnapshot() {
4652
+ return this._data.entitiesSnapshot;
4653
+ }
4654
+ get status() {
4655
+ return this._status;
4656
+ }
4657
+ get statusSnapshot() {
4658
+ return this._status.getValue();
4659
+ }
4660
+ get isEmpty() {
4661
+ return this.dataSnapshot.length === 0;
4662
+ }
4663
+ get isStarted() {
4664
+ return this.started;
4665
+ }
4666
+ get isStarted$() {
4667
+ return this._started.asObservable();
4668
+ }
4669
+ get isClosed() {
4670
+ return this._closed.getValue();
4762
4671
  }
4763
- get hasMoreData() {
4764
- return this._hasMoreData.asObservable();
4672
+ get started() {
4673
+ return this._started.getValue();
4765
4674
  }
4766
- loadMore() {
4767
- if (this.loadingSnapshot) {
4768
- this.logger.debug(this.id + ': Skipping load-more since already loading a chunk!');
4769
- return EMPTY;
4770
- }
4771
- const token = this._expectedChunkToken;
4772
- if (token && token.length > 0) {
4773
- return this.fetchNextChunk(token);
4774
- }
4775
- else {
4776
- this.logger.debug(this.id + ': Cannot load more data, since no more data available.');
4777
- return EMPTY;
4778
- }
4675
+ set started(started) {
4676
+ this._started.next(started);
4779
4677
  }
4780
4678
  /***************************************************************************
4781
4679
  * *
4782
- * Protect API *
4680
+ * Public API *
4783
4681
  * *
4784
4682
  **************************************************************************/
4785
- clearAll(silent = false) {
4786
- super.clearAll(silent);
4787
- this._chunkCache.clear();
4788
- this._hasMoreData.next(true);
4789
- this._expectedChunkToken = undefined;
4683
+ start(sorts, filters) {
4684
+ if (this.isClosed) {
4685
+ throw new Error(this.id + ': Restarting a closed DataContext is not possible!'); // See unsubscribe$
4686
+ }
4687
+ this.started = false;
4688
+ this._sort.replaceSorts(sorts || this.sort.sortsSnapshot);
4689
+ this._filter.replaceFilters(filters || this.filter.filtersSnapshot);
4690
+ this.baselog.debug(this.id + ': Start ...', {
4691
+ dataSource: this.dataSource,
4692
+ sort: this._sort.sortsSnapshot,
4693
+ filter: this._filter.filtersSnapshot,
4694
+ });
4695
+ this.started = true;
4696
+ return this.reload('START');
4790
4697
  }
4791
- reloadInternal() {
4792
- // Since continuable data-contexts are appending data,
4793
- // we need to clear it for a reload.
4794
- this.clearAll(true);
4795
- return this.fetchNextChunk(undefined);
4698
+ reload(reason) {
4699
+ const request = new ReloadRequest(++this._reloadCounter, reason);
4700
+ this.baselog.debug(this.id + ': Enqueuing reload request #' + request.number + ' [' + reason + ']');
4701
+ this._reloadQueue.next(request);
4702
+ return this._reloaded.pipe(filter((reload) => reload.fulfills(request)), map((reload) => reload.result));
4796
4703
  }
4797
- fetchNextChunk(nextToken) {
4798
- const subject = new Subject();
4799
- nextToken = nextToken ? nextToken : undefined;
4800
- if (this._chunkCache.has(nextToken)) {
4801
- this.logger.debug(this.id +
4802
- ': Skipping fetching chunk for token "' +
4803
- nextToken +
4804
- '" since its already in observable cache.');
4805
- subject.complete();
4806
- }
4807
- else {
4808
- this.onLoading();
4809
- this._chunkCache.add(nextToken);
4810
- this.dataSource
4811
- .findAllContinuable(new TokenChunkRequest(nextToken, this.filter.filtersSnapshot, this.sort.sortsSnapshot, this.chunkSize))
4812
- .pipe(first())
4813
- .subscribe({
4814
- next: (chunk) => {
4815
- this.onChunkFetched(chunk);
4816
- subject.next(chunk);
4817
- },
4818
- error: (err) => {
4819
- this.onChunkFetchError(err);
4820
- subject.error(err);
4821
- },
4822
- });
4704
+ findByIndex(key) {
4705
+ if (!this._indexFn) {
4706
+ throw new Error(this.id + ': findByIndex requires you to pass a index function!');
4823
4707
  }
4824
- return subject.pipe(take(1));
4825
- }
4826
- onChunkFetched(chunk) {
4827
- this.logger.debug(this.id + ': Got next chunk data:', chunk);
4828
- this._hasMoreData.next(chunk.hasMore);
4829
- this.updateChunkSize(chunk.chunkSize ?? chunk.maxChunkSize, false);
4830
- this.populateChunkData(chunk);
4831
- this.onIdle();
4708
+ return this._customIndex.get(key);
4832
4709
  }
4833
- onChunkFetchError(err) {
4834
- this.onError(err);
4835
- this.logger.error(this.id + ': Failed to query data', err);
4836
- this.clearData();
4837
- this.setTotal(0);
4710
+ findById(id) {
4711
+ return this._data.getById(id);
4838
4712
  }
4839
4713
  /**
4840
- * Load the data from the given page into the current data context
4714
+ * Closes this DataContext and cleans up all used resources.
4841
4715
  */
4842
- populateChunkData(chunk) {
4843
- if (this.areTokenEqual(chunk.continuationToken, this._expectedChunkToken)) {
4844
- try {
4845
- this.setTotal(chunk.total);
4846
- if (chunk.continuationToken) {
4847
- // We had previous chunks so append to current data.
4848
- this.appendData(chunk.content);
4849
- }
4850
- else {
4851
- this.setData(chunk.content);
4852
- }
4853
- }
4854
- catch (err) {
4855
- this.onError(err);
4856
- this.logger.error(this.id + ': Failed to populate data with chunk', chunk, err);
4857
- }
4858
- this._expectedChunkToken = chunk.nextContinuationToken;
4859
- }
4860
- else {
4861
- this.logger.warn(this.id +
4862
- ': Discarding continuable chunk (items: ' +
4863
- chunk.content.length +
4864
- ', token: ' +
4865
- chunk.continuationToken +
4866
- ' )' +
4867
- ' as it does not match the expected contiunation-token: ' +
4868
- this._expectedChunkToken);
4869
- }
4716
+ close() {
4717
+ this.started = false;
4718
+ this.destroy$.next();
4719
+ this.destroy$.complete();
4720
+ this._closed.next(true);
4721
+ this._data.destroy();
4722
+ this._total.complete();
4723
+ this._status.complete();
4724
+ this.clearAll();
4725
+ this.baselog.debug(this.id + ': Has been closed and resources cleaned up!');
4870
4726
  }
4871
- areTokenEqual(token1, token2) {
4872
- if (!token1 && !token2) {
4873
- return true;
4874
- }
4875
- return token1 === token2;
4727
+ refresh() {
4728
+ this._data.notify();
4876
4729
  }
4877
- }
4878
-
4879
- class DataContextActivePage extends DataContextBase {
4880
- /***************************************************************************
4881
- * *
4882
- * Constructor *
4883
- * *
4884
- **************************************************************************/
4885
- constructor(dataSource, pageSize, indexFn, localApply, localSort) {
4886
- super(dataSource, indexFn, localApply, localSort);
4887
- /***************************************************************************
4888
- * *
4889
- * Fields *
4890
- * *
4891
- **************************************************************************/
4892
- this.actlogger = LoggerFactory.getLogger(this.constructor.name);
4893
- this._page = new BehaviorSubject(new PageRequest(0, pageSize));
4730
+ update(entities) {
4731
+ this._data.updateSome(entities);
4894
4732
  }
4895
4733
  /***************************************************************************
4896
4734
  * *
4897
- * Properties *
4735
+ * Protected methods *
4898
4736
  * *
4899
4737
  **************************************************************************/
4900
- get dataSource() {
4901
- return super.dataSource;
4902
- }
4903
- get page() {
4904
- return this._page.asObservable();
4738
+ reloadNow(request) {
4739
+ this.baselog.debug(this.id + ': reloadNow triggered. Request #' + request.number + ' ' + request.reason, {
4740
+ filter: this._filter.filtersSnapshot,
4741
+ sort: this._sort.sortsSnapshot,
4742
+ dataSource: this.dataSource,
4743
+ request: request,
4744
+ });
4745
+ return this.reloadInternal(); // TODO This should actually return the real (http) request
4905
4746
  }
4906
- get pageSnapshot() {
4907
- return this._page.getValue();
4747
+ /**
4748
+ * Append the given rows to the existing ones.
4749
+ */
4750
+ appendData(additionalData) {
4751
+ additionalData = this.localApply(additionalData);
4752
+ this.indexAll(additionalData);
4753
+ this._data.append(additionalData);
4908
4754
  }
4909
- /***************************************************************************
4910
- * *
4911
- * Public API *
4912
- * *
4913
- **************************************************************************/
4914
4755
  /**
4915
- * Set the current page index and reload.
4916
- * @param pageIndex
4756
+ * Insert the given rows at the given location.
4917
4757
  */
4918
- setActiveIndex(pageIndex) {
4919
- this.setActivePage(new PageRequest(pageIndex, this.pageSnapshot.size));
4758
+ insertData(additionalData, offset) {
4759
+ additionalData = this.localApply(additionalData);
4760
+ this.indexAll(additionalData);
4761
+ this._data.insert(additionalData, offset);
4920
4762
  }
4921
4763
  /**
4922
- * Sets the current page index / size and reload.
4764
+ * Replaces the existing data with the new set.
4765
+ * @param newData The new data set.
4766
+ * @param alreadyIndexed The rows will be indexed unless they already have been.
4923
4767
  */
4924
- setActivePage(request) {
4925
- if (!request) {
4926
- throw new Error(this.id + ': Setting page PageRequest must not be null!');
4768
+ setData(newData, alreadyIndexed = false) {
4769
+ newData = this.localApply(newData);
4770
+ if (!alreadyIndexed) {
4771
+ this.clearIndex();
4772
+ this.indexAll(newData);
4927
4773
  }
4928
- let hasChange = false;
4929
- const page = this.pageSnapshot;
4930
- if (page.index !== request.index) {
4931
- hasChange = true;
4774
+ this._data.replaceAll(newData);
4775
+ }
4776
+ clearData() {
4777
+ this.setData([]);
4778
+ }
4779
+ localApply(data) {
4780
+ if (this._localApply) {
4781
+ return this._localApply(data);
4932
4782
  }
4933
- else if (page.size !== request.size) {
4934
- hasChange = true;
4783
+ else {
4784
+ return data;
4935
4785
  }
4936
- if (hasChange) {
4937
- this._page.next(request);
4938
- this.reload('setActivePage');
4786
+ }
4787
+ setTotal(total) {
4788
+ this._total.next(total);
4789
+ }
4790
+ /**
4791
+ * Clears the current data-context cached data.
4792
+ * State such as current sorting and filtersSnapshot are kept.
4793
+ */
4794
+ clearAll(silent = false) {
4795
+ this.clearIndex();
4796
+ if (!silent) {
4797
+ this.setTotal(0);
4798
+ this.clearData();
4799
+ }
4800
+ this.onIdle();
4801
+ }
4802
+ updateIndex() {
4803
+ this.clearIndex();
4804
+ this.indexAll(this.dataSnapshot);
4805
+ }
4806
+ /**
4807
+ * Indexes the given item by key if there is an index function defined.
4808
+ */
4809
+ indexItem(item) {
4810
+ const key = this.getItemKey(item);
4811
+ if (key) {
4812
+ this._customIndex.set(key, item);
4939
4813
  }
4940
4814
  }
4941
- close() {
4942
- super.close();
4943
- if (this._activePageChangedSub) {
4944
- this._activePageChangedSub.unsubscribe();
4815
+ /**
4816
+ * Indexes all the given items by key if there is an index function defined.
4817
+ */
4818
+ indexAll(items) {
4819
+ if (this._indexFn) {
4820
+ items.forEach((item) => this.indexItem(item));
4945
4821
  }
4946
4822
  }
4947
- /***************************************************************************
4948
- * *
4949
- * Private methods *
4950
- * *
4951
- **************************************************************************/
4952
- clearAll() {
4953
- super.clearAll();
4954
- this.setActiveIndex(0);
4823
+ getItemKey(item) {
4824
+ if (this._indexFn) {
4825
+ return this._indexFn(item);
4826
+ }
4827
+ return null;
4955
4828
  }
4956
- reloadInternal() {
4957
- if (this._activePageLoad) {
4958
- // Cancel previous pending request
4959
- this._activePageLoad.unsubscribe();
4829
+ getItemId(item) {
4830
+ return this.dataSource.getId(item);
4831
+ }
4832
+ getDebounceTime() {
4833
+ if (isLocalDataSource(this.dataSource)) {
4834
+ return 0;
4960
4835
  }
4961
- const subject = new Subject();
4962
- this.onLoading();
4963
- const page = this.pageSnapshot;
4964
- const pageRequest = new Pageable(page.index, page.size, this.sort.sortsSnapshot);
4965
- this._activePageLoad = this.dataSource
4966
- .findAllPaged(pageRequest, this.filter.filtersSnapshot)
4967
- .pipe(first())
4968
- .subscribe((success) => {
4969
- this.setTotal(success.totalElements);
4970
- this.setData(success.content);
4971
- subject.next(success);
4972
- this.onIdle();
4973
- }, (err) => {
4974
- this.clearData();
4975
- this.actlogger.error(this.id + ': Failed to query data', err);
4976
- subject.error(err);
4977
- this.onError(err);
4978
- }, () => {
4979
- this.onIdle();
4980
- });
4981
- return subject.pipe(first());
4836
+ return DEFAULT_DEBOUNCE_TIME$1;
4982
4837
  }
4983
4838
  /***************************************************************************
4984
4839
  * *
4985
- * Event handlers *
4840
+ * Event handler *
4986
4841
  * *
4987
4842
  **************************************************************************/
4988
4843
  /**
4989
- * Occurs when the sorts property has changed.
4844
+ * Occurs when the sort has changed.
4990
4845
  */
4991
4846
  onSortsChanged(sorts) {
4992
- this.setActiveIndex(0);
4993
- super.onSortsChanged(sorts);
4847
+ if (!this._localSort) {
4848
+ this.reload('Sort Changed');
4849
+ }
4850
+ else {
4851
+ this.setData(this.dataSnapshot, true);
4852
+ }
4994
4853
  }
4995
4854
  /**
4996
- * Occurs when the filtersSnapshot property has changed.
4855
+ * Occurs when the filter has changed.
4997
4856
  */
4998
4857
  onFiltersChanged(filters) {
4999
- this.setActiveIndex(0);
5000
- super.onFiltersChanged(filters);
4858
+ this.reload('Filters Changed');
4859
+ }
4860
+ onError(err) {
4861
+ this.onStatus(DataContextStatus.error(err));
4862
+ }
4863
+ onIdle() {
4864
+ this.onStatus(DataContextStatus.idle());
4865
+ }
4866
+ onLoading() {
4867
+ this.onStatus(DataContextStatus.loading());
4868
+ }
4869
+ onStatus(status) {
4870
+ this._status.next(status);
4871
+ }
4872
+ /***************************************************************************
4873
+ * *
4874
+ * Private methods *
4875
+ * *
4876
+ **************************************************************************/
4877
+ clearIndex() {
4878
+ this._customIndex.clear();
5001
4879
  }
5002
4880
  }
5003
4881
 
5004
- class MatTableDataContextBindingBuilder {
4882
+ class DataContextSimple extends DataContextBase {
5005
4883
  /***************************************************************************
5006
4884
  * *
5007
- * Static Builder *
4885
+ * Constructor *
5008
4886
  * *
5009
4887
  **************************************************************************/
5010
- static start(dataContext$) {
5011
- return new MatTableDataContextBindingBuilder(dataContext$);
5012
- }
5013
- constructor(dataContext$) {
4888
+ constructor(dataSource, indexFn, localApply, localSort) {
4889
+ super(dataSource, indexFn, localApply, localSort);
5014
4890
  /***************************************************************************
5015
4891
  * *
5016
4892
  * Fields *
5017
4893
  * *
5018
4894
  **************************************************************************/
5019
- this.logger = LoggerFactory.getLogger(this.constructor.name);
5020
- this._dataContext$ = dataContext$;
4895
+ this.log = LoggerFactory.getLogger(this.constructor.name);
5021
4896
  }
5022
4897
  /***************************************************************************
5023
4898
  * *
5024
- * Public API *
4899
+ * Properties *
5025
4900
  * *
5026
4901
  **************************************************************************/
5027
- withSorts(sorts$) {
5028
- this._sorts$ = sorts$;
5029
- return this;
5030
- }
5031
- withPaginator(paginator$) {
5032
- this._matPaginator$ = paginator$;
5033
- return this;
5034
- }
5035
- withContinuator(continuator$) {
5036
- this._continuator$ = continuator$;
5037
- return this;
4902
+ get dataSource() {
4903
+ return super.dataSource;
5038
4904
  }
5039
- bindUntil(destroy$) {
5040
- return new MatTableDataContextBinding(this._dataContext$, this._sorts$, this._matPaginator$, this._continuator$, destroy$);
4905
+ /***************************************************************************
4906
+ * *
4907
+ * Private methods *
4908
+ * *
4909
+ **************************************************************************/
4910
+ reloadInternal() {
4911
+ const subject = new Subject();
4912
+ if (this.dataSource) {
4913
+ this.onLoading();
4914
+ this.dataSource
4915
+ .findAllFiltered(this.filter.filtersSnapshot, this.sort.sortsSnapshot)
4916
+ .pipe(first())
4917
+ .subscribe((list) => {
4918
+ this.onIdle();
4919
+ this.setTotal(list.length);
4920
+ this.setData(list);
4921
+ this.log.debug(this.id + ': Got list data: ' + list.length);
4922
+ subject.next();
4923
+ }, (err) => {
4924
+ this.onError(err);
4925
+ this.clearAll();
4926
+ this.log.error(this.id + ': Failed to query data', err);
4927
+ subject.error(err);
4928
+ });
4929
+ }
4930
+ else {
4931
+ const errorMsg = this.id + ': Skipping data context load - no list fetcher present!';
4932
+ this.log.warn(errorMsg);
4933
+ subject.error(new Error(errorMsg));
4934
+ }
4935
+ return subject.pipe(first());
5041
4936
  }
5042
4937
  }
5043
- class MatTableDataContextBinding {
4938
+
4939
+ /**
4940
+ * Extends a simple flat list data-context with infinite-scroll pagination support.
4941
+ *
4942
+ */
4943
+ class DataContextContinuableBase extends DataContextBase {
5044
4944
  /***************************************************************************
5045
4945
  * *
5046
- * Constructor *
4946
+ * Constructors *
5047
4947
  * *
5048
4948
  **************************************************************************/
5049
- constructor(_dataContext$, _matSorts$, _matPaginator$, _continuator$, destroy$) {
5050
- this._dataContext$ = _dataContext$;
5051
- this._matSorts$ = _matSorts$;
5052
- this._matPaginator$ = _matPaginator$;
5053
- this._continuator$ = _continuator$;
4949
+ constructor(dataSource, chunkSize, indexFn, localApply, localSort) {
4950
+ super(dataSource, indexFn, localApply, localSort);
5054
4951
  /***************************************************************************
5055
4952
  * *
5056
4953
  * Fields *
5057
4954
  * *
5058
4955
  **************************************************************************/
5059
- this.logger = LoggerFactory.getLogger(this.constructor.name);
5060
- this.subscribeUntil(destroy$);
4956
+ this.cblogger = LoggerFactory.getLogger(this.constructor.name);
4957
+ this._chunkSize$ = new BehaviorSubject(chunkSize);
5061
4958
  }
5062
4959
  /***************************************************************************
5063
4960
  * *
5064
- * Private methods *
4961
+ * Public API *
5065
4962
  * *
5066
4963
  **************************************************************************/
5067
- subscribeUntil(destroy$) {
5068
- if (this._matSorts$) {
5069
- this.bindMatSortsToDataContextUntil(this._matSorts$, destroy$);
5070
- this.bindDataContextToMatSortsUntil(this._matSorts$, destroy$);
5071
- }
5072
- if (this._matPaginator$) {
5073
- this.bindPaginatorUntil(this._matPaginator$, destroy$);
5074
- }
5075
- if (this._continuator$) {
5076
- this.bindContinuatorUntil(this._continuator$, destroy$);
5077
- }
5078
- }
5079
- bindDataContextToMatSortsUntil(matSorts$, destroy$) {
5080
- const dcSorts$ = this._dataContext$.pipe(filter((dc) => !!dc), switchMap$1((dc) => dc.sort.sorts));
5081
- combineLatest([dcSorts$, matSorts$])
5082
- .pipe(takeUntil(destroy$))
5083
- .subscribe(([dcSorts, matSorts]) => {
5084
- if (dcSorts.length >= 1) {
5085
- // At least one sort active
5086
- const sort = dcSorts[0];
5087
- this.updateMatSorts(sort, matSorts);
5088
- }
5089
- else {
5090
- // No sort active
5091
- this.updateMatSorts(Sort.NONE, matSorts);
5092
- }
5093
- });
5094
- }
5095
- updateMatSorts(dcSort, matSorts) {
5096
- const active = dcSort.prop;
5097
- const direction = this.toMatDirection(dcSort.dir);
5098
- matSorts.forEach((matSort) => this.updateMatSort(active, direction, matSort));
5099
- }
5100
- updateMatSort(activeProp, direction, matSort) {
5101
- if (activeProp) {
5102
- if (!matSort.sortables.has(activeProp)) {
5103
- // The current sort property is not part of this MatSort.
5104
- activeProp = Sort.NONE.prop; // Force no sort in this mat context
5105
- }
5106
- }
5107
- if (matSort.active !== activeProp || matSort.direction !== direction) {
5108
- // We do only update matSort when there was a real change
5109
- matSort.active = activeProp;
5110
- matSort.direction = direction;
5111
- matSort._stateChanges.next();
5112
- }
5113
- }
5114
- bindMatSortsToDataContextUntil(sorts$, destroy$) {
5115
- const sortChanges$ = sorts$.pipe(mergeMap((sorts) => merge(...sorts.map((s) => s.sortChange))), map((matSort) => {
5116
- return new Sort(matSort.active, this.fromMatDirection(matSort.direction));
5117
- }));
5118
- combineLatest([this._dataContext$, sortChanges$])
5119
- .pipe(takeUntil(destroy$))
5120
- .subscribe(([dc, sortRequest]) => {
5121
- if (dc) {
5122
- dc.sort.updateSort(sortRequest);
5123
- }
5124
- });
5125
- }
5126
- bindPaginatorUntil(paginator$, destroy$) {
5127
- const pageRequest$ = paginator$.pipe(filter((paginator) => !!paginator), switchMap$1((paginator) => paginator.page), map((pageEvent) => new PageRequest(pageEvent.pageIndex, pageEvent.pageSize)));
5128
- combineLatest([this._dataContext$, pageRequest$])
5129
- .pipe(takeUntil(destroy$))
5130
- .subscribe(([dc, pageRequest]) => {
5131
- if (dc) {
5132
- if (isActivePagedDataContext(dc)) {
5133
- const pagedDc = dc;
5134
- pagedDc.setActivePage(pageRequest);
5135
- }
5136
- else {
5137
- this.logger.warn('Can not bind the given paginator to the given data-context,' +
5138
- ' as the datacontext does not support pagination!', dc);
5139
- }
5140
- }
4964
+ loadAll(sorts, filters) {
4965
+ this.cblogger.debug(this.id + ': Starting to load all data ...');
4966
+ // load first page
4967
+ this.start(sorts, filters).subscribe({
4968
+ next: () => {
4969
+ this.cblogger.debug(this.id + ': First page has been loaded. Loading remaining data ...');
4970
+ // load rest in a recursive manner
4971
+ this.loadAllRec();
4972
+ },
4973
+ error: (err) => {
4974
+ this.onError(err);
4975
+ this.cblogger.error(this.id + ': Failed to load first page of load all procedure!', err);
4976
+ },
5141
4977
  });
5142
4978
  }
5143
- bindContinuatorUntil(continuator$, destroy$) {
5144
- const chunkSizeChange$ = continuator$.pipe(filter((continuator) => !!continuator), switchMap$1((continuator) => outputToObservable(continuator.chunkSizeChange)));
5145
- combineLatest([this._dataContext$, chunkSizeChange$])
5146
- .pipe(takeUntil(destroy$))
5147
- .subscribe(([dc, newChunkSize]) => {
5148
- if (dc) {
5149
- if (isContinuableDataContext(dc)) {
5150
- const continuableDc = dc;
5151
- continuableDc.chunkSize = newChunkSize;
5152
- }
5153
- else {
5154
- this.logger.warn('Can not bind the given Continuator to the given data-context,' +
5155
- ' as the DataContext does not support continuation!', dc);
5156
- }
5157
- }
5158
- });
4979
+ get chunkSize$() {
4980
+ return this._chunkSize$.asObservable();
5159
4981
  }
5160
- toMatDirection(direction) {
5161
- return direction;
4982
+ get chunkSize() {
4983
+ return this._chunkSize$.value;
5162
4984
  }
5163
- fromMatDirection(matSortDirection) {
5164
- return matSortDirection;
4985
+ set chunkSize(size) {
4986
+ this.updateChunkSize(size, true);
4987
+ }
4988
+ /***************************************************************************
4989
+ * *
4990
+ * Private Methods *
4991
+ * *
4992
+ **************************************************************************/
4993
+ updateChunkSize(newSize, reloadOnChange) {
4994
+ if (this._chunkSize$.value !== newSize) {
4995
+ this._chunkSize$.next(newSize);
4996
+ if (reloadOnChange) {
4997
+ this.reload('ChunkSizeChanged:' + newSize);
4998
+ }
4999
+ }
5000
+ }
5001
+ loadAllRec() {
5002
+ this.loadMore().subscribe({
5003
+ next: () => {
5004
+ this.cblogger.debug(this.id + ': Loading data chunk finished, loading next...');
5005
+ this.loadAllRec();
5006
+ },
5007
+ error: (err) => {
5008
+ this.onError(err);
5009
+ this.cblogger.error(this.id + ': Loading all failed!', err);
5010
+ },
5011
+ complete: () => {
5012
+ this.cblogger.info(this.id + ': All data loaded completely.');
5013
+ },
5014
+ });
5165
5015
  }
5166
5016
  }
5167
5017
 
5168
5018
  /**
5169
- * Allows making any kind of subscription which will be automatically unsubscribed
5170
- * upon data context closing/cleanup.
5019
+ * Extends a simple flat list data-context with infinite-scroll pagination support.
5020
+ *
5171
5021
  */
5172
- class DataContextLifeCycleBinding {
5022
+ class DataContextContinuablePaged extends DataContextContinuableBase {
5173
5023
  /***************************************************************************
5174
5024
  * *
5175
- * Constructor *
5025
+ * Constructors *
5176
5026
  * *
5177
5027
  **************************************************************************/
5178
- constructor(_dataContext) {
5179
- this._dataContext = _dataContext;
5180
- // eslint-disable-next-line eqeqeq
5181
- if (_dataContext == null) {
5182
- throw new Error('dataContext must not be null!');
5183
- }
5184
- this._dataContext.data.subscribe({
5185
- complete: () => this.unsubscribe(),
5186
- });
5028
+ constructor(dataSource, pageSize, indexFn, localApply, localSort) {
5029
+ super(dataSource, pageSize, indexFn, localApply, localSort);
5030
+ /***************************************************************************
5031
+ * *
5032
+ * Fields *
5033
+ * *
5034
+ **************************************************************************/
5035
+ this.logger = LoggerFactory.getLogger(this.constructor.name);
5036
+ this._pageCache = new Map();
5037
+ this._latestPage = 0;
5038
+ this._hasMoreData = combineLatest([this.total, this.data]).pipe(map(([total, data]) => this.checkHasMoreData(total, data)), takeUntil(this.destroy$));
5039
+ }
5040
+ /***************************************************************************
5041
+ * *
5042
+ * Properties *
5043
+ * *
5044
+ **************************************************************************/
5045
+ get dataSource() {
5046
+ return super.dataSource;
5187
5047
  }
5188
5048
  /***************************************************************************
5189
5049
  * *
5190
5050
  * Public API *
5191
5051
  * *
5192
5052
  **************************************************************************/
5193
- unsubscribe() {
5194
- if (this._subscription) {
5195
- this._subscription.unsubscribe();
5196
- this._subscription = null;
5053
+ /**
5054
+ * Load the next chunk of data.
5055
+ * Useful for infinite scroll like data flows.
5056
+ *
5057
+ */
5058
+ loadMore() {
5059
+ if (this.hasMoreDataSnapshot) {
5060
+ this.logger.info(this.id + ': Loading more...' + this._latestPage);
5061
+ if (this.loadingSnapshot) {
5062
+ return EMPTY;
5063
+ }
5064
+ const nextPage = this._latestPage + 1;
5065
+ return this.fetchPage(nextPage, this.chunkSize);
5066
+ }
5067
+ else {
5068
+ this.logger.debug(this.id + ': Cannot load more data, since no more data available.');
5069
+ return EMPTY;
5197
5070
  }
5198
5071
  }
5199
- }
5200
-
5201
- class RequiredFilterContextChangedEvent {
5202
- constructor(requiredFilters, currentFilters, isValid) {
5203
- this.requiredFilters = requiredFilters;
5204
- this.currentFilters = currentFilters;
5205
- this.isValid = isValid;
5072
+ get hasMoreData() {
5073
+ return this._hasMoreData;
5074
+ }
5075
+ get hasMoreDataSnapshot() {
5076
+ return this.checkHasMoreData(this.totalSnapshot, this.dataSnapshot);
5206
5077
  }
5207
- }
5208
- class RequiredFilterEvaluator {
5209
5078
  /***************************************************************************
5210
5079
  * *
5211
- * Fields *
5080
+ * Private Methods *
5212
5081
  * *
5213
5082
  **************************************************************************/
5214
- constructor(filterContext, requiredFilters) {
5215
- this._requiredFilters = new BehaviorSubject([]);
5216
- // eslint-disable-next-line eqeqeq
5217
- if (filterContext == null) {
5218
- throw new Error('filterContext must not be null!');
5083
+ checkHasMoreData(total, data) {
5084
+ if (total) {
5085
+ return total > data.length;
5219
5086
  }
5220
- this._filterContext = filterContext;
5221
- if (requiredFilters) {
5222
- this._requiredFilters.next(requiredFilters);
5087
+ return false;
5088
+ }
5089
+ reloadInternal() {
5090
+ // Since continuable data-contexts are appending data,
5091
+ // we need to clear it for a reload.
5092
+ this.clearAll(true);
5093
+ return this.fetchPage(0, this.chunkSize, true);
5094
+ }
5095
+ clearAll(silent = false) {
5096
+ super.clearAll(silent);
5097
+ this._pageCache = new Map();
5098
+ this._latestPage = 0;
5099
+ }
5100
+ fetchPage(pageIndex, pageSize, clear = false) {
5101
+ const subject = new Subject();
5102
+ const pageRequest = new Pageable(pageIndex, pageSize, this.sort.sortsSnapshot);
5103
+ if (this._pageCache.has(pageIndex)) {
5104
+ // Page already loaded - skipping request!
5105
+ this.logger.debug(this.id + ': Skipping fetching page since its already in page observable cache.');
5106
+ subject.next();
5223
5107
  }
5224
- this.context$ = combineLatest([this._requiredFilters, this._filterContext.filters]).pipe(map(([required, currentFilters]) => this.createEvent(requiredFilters, currentFilters)));
5108
+ else {
5109
+ this.onLoading();
5110
+ this.logger.debug(this.id + `: Loading page ${pageIndex} using pageable:`, pageRequest);
5111
+ const pageObs = this.dataSource.findAllPaged(pageRequest, this.filter.filtersSnapshot);
5112
+ this._pageCache.set(pageIndex, pageObs);
5113
+ pageObs.subscribe((page) => {
5114
+ this.logger.debug(this.id + ': Got page data:', page);
5115
+ this.populatePageData(page, clear);
5116
+ if (this._latestPage < page.number) {
5117
+ this._latestPage = page.number; // TODO This might cause that pages are skipped
5118
+ }
5119
+ subject.next();
5120
+ this.onIdle();
5121
+ }, (err) => {
5122
+ this.onError(err);
5123
+ this.clearData();
5124
+ this.setTotal(0);
5125
+ this.logger.error(this.id + ': Failed to query data', err);
5126
+ subject.error(err);
5127
+ });
5128
+ }
5129
+ return subject.pipe(first());
5130
+ }
5131
+ /**
5132
+ * Load the data from the given page into the current data context
5133
+ */
5134
+ populatePageData(page, clear) {
5135
+ try {
5136
+ this.setTotal(page.totalElements);
5137
+ const start = page.number * page.size;
5138
+ if (clear) {
5139
+ this.setData(page.content);
5140
+ }
5141
+ else {
5142
+ this.insertData(page.content, start);
5143
+ }
5144
+ }
5145
+ catch (err) {
5146
+ this.onError(err);
5147
+ this.logger.error(this.id + ': Failed to populate data with page', page, err);
5148
+ }
5149
+ }
5150
+ }
5151
+
5152
+ class DataContextContinuableToken extends DataContextContinuableBase {
5153
+ /***************************************************************************
5154
+ * *
5155
+ * Constructor *
5156
+ * *
5157
+ **************************************************************************/
5158
+ constructor(dataSource, chunkSize, indexFn, localApply, localSort) {
5159
+ super(dataSource, chunkSize, indexFn, localApply, localSort);
5160
+ /***************************************************************************
5161
+ * *
5162
+ * Fields *
5163
+ * *
5164
+ **************************************************************************/
5165
+ this.logger = LoggerFactory.getLogger(this.constructor.name);
5166
+ this._hasMoreData = new BehaviorSubject(false);
5167
+ this._chunkCache = new Set();
5225
5168
  }
5226
5169
  /***************************************************************************
5227
5170
  * *
5228
5171
  * Properties *
5229
5172
  * *
5230
5173
  **************************************************************************/
5231
- get filterContext() {
5232
- return this._filterContext;
5174
+ get dataSource() {
5175
+ return super.dataSource;
5233
5176
  }
5234
- get requiredFilters$() {
5235
- return this._requiredFilters.asObservable();
5177
+ /***************************************************************************
5178
+ * *
5179
+ * Public API *
5180
+ * *
5181
+ **************************************************************************/
5182
+ get hasMoreDataSnapshot() {
5183
+ return this._hasMoreData.getValue();
5236
5184
  }
5237
- get requiredFilters() {
5238
- return this._requiredFilters.getValue();
5185
+ get hasMoreData() {
5186
+ return this._hasMoreData.asObservable();
5187
+ }
5188
+ loadMore() {
5189
+ if (this.loadingSnapshot) {
5190
+ this.logger.debug(this.id + ': Skipping load-more since already loading a chunk!');
5191
+ return EMPTY;
5192
+ }
5193
+ const token = this._expectedChunkToken;
5194
+ if (token && token.length > 0) {
5195
+ return this.fetchNextChunk(token);
5196
+ }
5197
+ else {
5198
+ this.logger.debug(this.id + ': Cannot load more data, since no more data available.');
5199
+ return EMPTY;
5200
+ }
5239
5201
  }
5240
5202
  /***************************************************************************
5241
5203
  * *
5242
- * Public API *
5243
- * *
5244
- **************************************************************************/
5245
- /***************************************************************************
5246
- * *
5247
- * Private methods *
5204
+ * Protect API *
5248
5205
  * *
5249
5206
  **************************************************************************/
5250
- createEvent(requiredFilters, currentFilters) {
5251
- return new RequiredFilterContextChangedEvent(requiredFilters, currentFilters, this.allRequiredFiltersPresent(requiredFilters, currentFilters));
5252
- }
5253
- allRequiredFiltersPresent(requiredFilters, currentFilters) {
5254
- if (requiredFilters) {
5255
- return requiredFilters.some((filterGroup) => this.hasAllFilters(filterGroup, currentFilters));
5256
- }
5257
- return true;
5207
+ clearAll(silent = false) {
5208
+ super.clearAll(silent);
5209
+ this._chunkCache.clear();
5210
+ this._hasMoreData.next(true);
5211
+ this._expectedChunkToken = undefined;
5258
5212
  }
5259
- hasAllFilters(requiredFilters, currentFilters) {
5260
- const requiredFilterSet = new Set(requiredFilters);
5261
- const currentFilterKeySet = new Set(currentFilters.map((f) => f.key));
5262
- const areAllRequiredFiltersPresent = requiredFilters.every((required) => currentFilterKeySet.has(required));
5263
- const areAllRequiredFiltersHaveValue = currentFilters
5264
- .filter((f) => requiredFilterSet.has(f.key))
5265
- .every((currentFilter) => Filter.hasValue(currentFilter));
5266
- return areAllRequiredFiltersPresent && areAllRequiredFiltersHaveValue;
5213
+ reloadInternal() {
5214
+ // Since continuable data-contexts are appending data,
5215
+ // we need to clear it for a reload.
5216
+ this.clearAll(true);
5217
+ return this.fetchNextChunk(undefined);
5267
5218
  }
5268
- }
5269
-
5270
- class AutoStartSpec {
5271
- static asap(sort) {
5272
- return new AutoStartSpec(null, sort);
5219
+ fetchNextChunk(nextToken) {
5220
+ const subject = new Subject();
5221
+ nextToken = nextToken ? nextToken : undefined;
5222
+ if (this._chunkCache.has(nextToken)) {
5223
+ this.logger.debug(this.id +
5224
+ ': Skipping fetching chunk for token "' +
5225
+ nextToken +
5226
+ '" since its already in observable cache.');
5227
+ subject.complete();
5228
+ }
5229
+ else {
5230
+ this.onLoading();
5231
+ this._chunkCache.add(nextToken);
5232
+ this.dataSource
5233
+ .findAllContinuable(new TokenChunkRequest(nextToken, this.filter.filtersSnapshot, this.sort.sortsSnapshot, this.chunkSize))
5234
+ .pipe(first())
5235
+ .subscribe({
5236
+ next: (chunk) => {
5237
+ this.onChunkFetched(chunk);
5238
+ subject.next(chunk);
5239
+ },
5240
+ error: (err) => {
5241
+ this.onChunkFetchError(err);
5242
+ subject.error(err);
5243
+ },
5244
+ });
5245
+ }
5246
+ return subject.pipe(take(1));
5273
5247
  }
5274
- static requireFiltersAll(filters, sort) {
5275
- return AutoStartSpec.requireFilters([filters], sort);
5248
+ onChunkFetched(chunk) {
5249
+ this.logger.debug(this.id + ': Got next chunk data:', chunk);
5250
+ this._hasMoreData.next(chunk.hasMore);
5251
+ this.updateChunkSize(chunk.chunkSize ?? chunk.maxChunkSize, false);
5252
+ this.populateChunkData(chunk);
5253
+ this.onIdle();
5276
5254
  }
5277
- static requireFiltersAny(filters, sort) {
5278
- const separateFilterArrays = filters.map((singleFilter) => [singleFilter]);
5279
- return AutoStartSpec.requireFilters(separateFilterArrays, sort);
5255
+ onChunkFetchError(err) {
5256
+ this.onError(err);
5257
+ this.logger.error(this.id + ': Failed to query data', err);
5258
+ this.clearData();
5259
+ this.setTotal(0);
5280
5260
  }
5281
- static requireFilters(filters, sort) {
5282
- return new AutoStartSpec(filters, sort);
5261
+ /**
5262
+ * Load the data from the given page into the current data context
5263
+ */
5264
+ populateChunkData(chunk) {
5265
+ if (this.areTokenEqual(chunk.continuationToken, this._expectedChunkToken)) {
5266
+ try {
5267
+ this.setTotal(chunk.total);
5268
+ if (chunk.continuationToken) {
5269
+ // We had previous chunks so append to current data.
5270
+ this.appendData(chunk.content);
5271
+ }
5272
+ else {
5273
+ this.setData(chunk.content);
5274
+ }
5275
+ }
5276
+ catch (err) {
5277
+ this.onError(err);
5278
+ this.logger.error(this.id + ': Failed to populate data with chunk', chunk, err);
5279
+ }
5280
+ this._expectedChunkToken = chunk.nextContinuationToken;
5281
+ }
5282
+ else {
5283
+ this.logger.warn(this.id +
5284
+ ': Discarding continuable chunk (items: ' +
5285
+ chunk.content.length +
5286
+ ', token: ' +
5287
+ chunk.continuationToken +
5288
+ ' )' +
5289
+ ' as it does not match the expected contiunation-token: ' +
5290
+ this._expectedChunkToken);
5291
+ }
5283
5292
  }
5284
- constructor(requiredFilters, initialSort) {
5285
- this.requiredFilters = requiredFilters;
5286
- this.initialSort = initialSort;
5293
+ areTokenEqual(token1, token2) {
5294
+ if (!token1 && !token2) {
5295
+ return true;
5296
+ }
5297
+ return token1 === token2;
5287
5298
  }
5288
5299
  }
5289
- class DataContextAutoStarter extends DataContextLifeCycleBinding {
5300
+
5301
+ class DataContextActivePage extends DataContextBase {
5290
5302
  /***************************************************************************
5291
5303
  * *
5292
5304
  * Constructor *
5293
5305
  * *
5294
5306
  **************************************************************************/
5295
- constructor(dataContext, _autoStartSpec) {
5296
- super(dataContext);
5297
- this._autoStartSpec = _autoStartSpec;
5307
+ constructor(dataSource, pageSize, indexFn, localApply, localSort) {
5308
+ super(dataSource, indexFn, localApply, localSort);
5298
5309
  /***************************************************************************
5299
5310
  * *
5300
5311
  * Fields *
5301
5312
  * *
5302
5313
  **************************************************************************/
5303
- this.logger = LoggerFactory.getLogger(this.constructor.name);
5304
- // eslint-disable-next-line eqeqeq
5305
- if (_autoStartSpec == null) {
5306
- throw new Error('autoStartSpec must not be null!');
5307
- }
5308
- if (_autoStartSpec.requiredFilters) {
5309
- this._autoStartConditionFulfilled$ = this.buildRequiredFilterConditionObservable();
5310
- }
5311
- else {
5312
- // no condition defined, try to start immediately
5313
- this.startDataContext();
5314
- }
5315
- this.subscribe();
5314
+ this.actlogger = LoggerFactory.getLogger(this.constructor.name);
5315
+ this._page = new BehaviorSubject(new PageRequest(0, pageSize));
5316
5316
  }
5317
5317
  /***************************************************************************
5318
5318
  * *
5319
- * Public API *
5319
+ * Properties *
5320
5320
  * *
5321
5321
  **************************************************************************/
5322
- subscribe() {
5323
- const hasRequiredFilters = !!this._autoStartConditionFulfilled$;
5324
- if (hasRequiredFilters) {
5325
- const startedTrue$ = this._dataContext.isStarted$.pipe(filter((t) => t));
5326
- this._subscription = this._autoStartConditionFulfilled$
5327
- .pipe(takeUntil(startedTrue$), tap((fulfilled) => this.logger.debug(`Got fulfilled event: ${fulfilled}`)), filter((fulfilled) => !!fulfilled))
5328
- .subscribe(() => this.startDataContext());
5329
- }
5322
+ get dataSource() {
5323
+ return super.dataSource;
5330
5324
  }
5331
- /***************************************************************************
5332
- * *
5333
- * Private methods *
5334
- * *
5335
- **************************************************************************/
5336
- buildRequiredFilterConditionObservable() {
5337
- return new RequiredFilterEvaluator(this._dataContext.filter, this._autoStartSpec.requiredFilters).context$.pipe(map((event) => event.isValid));
5325
+ get page() {
5326
+ return this._page.asObservable();
5338
5327
  }
5339
- startDataContext() {
5340
- this.logger.debug(this._dataContext.id + ': Auto starting ...');
5341
- this._dataContext.start(this._autoStartSpec.initialSort);
5328
+ get pageSnapshot() {
5329
+ return this._page.getValue();
5342
5330
  }
5343
- }
5344
-
5345
- class DeepPartialPatcher {
5346
5331
  /***************************************************************************
5347
5332
  * *
5348
5333
  * Public API *
5349
5334
  * *
5350
5335
  **************************************************************************/
5351
5336
  /**
5352
- * All modified parts are deep cloned so the original is never modified.
5337
+ * Set the current page index and reload.
5338
+ * @param pageIndex
5353
5339
  */
5354
- static patchDeepClone(original, patch) {
5355
- return this.mapReducePatch(original, patch);
5340
+ setActiveIndex(pageIndex) {
5341
+ this.setActivePage(new PageRequest(pageIndex, this.pageSnapshot.size));
5356
5342
  }
5357
5343
  /**
5358
- * Returns a shallow copy of the original, but may modify nested objects.
5344
+ * Sets the current page index / size and reload.
5359
5345
  */
5360
- static patchShallowCopy(original, patch) {
5361
- const shallowCopy = { ...original };
5362
- return this.patchOriginal(shallowCopy, patch);
5346
+ setActivePage(request) {
5347
+ if (!request) {
5348
+ throw new Error(this.id + ': Setting page PageRequest must not be null!');
5349
+ }
5350
+ let hasChange = false;
5351
+ const page = this.pageSnapshot;
5352
+ if (page.index !== request.index) {
5353
+ hasChange = true;
5354
+ }
5355
+ else if (page.size !== request.size) {
5356
+ hasChange = true;
5357
+ }
5358
+ if (hasChange) {
5359
+ this._page.next(request);
5360
+ this.reload('setActivePage');
5361
+ }
5363
5362
  }
5364
- /**
5365
- * Modifies the original directly. Returns a reference to the original object.
5366
- */
5367
- static patchOriginal(original, patch) {
5368
- const result = original;
5369
- for (const key in patch) {
5370
- if (patch[key] && typeof patch[key] === 'object' && !Array.isArray(patch[key])) {
5371
- // Recursively apply patch for nested objects
5372
- result[key] = DeepPartialPatcher.patchOriginal(result[key] ?? new Object(patch[key]), patch[key]);
5373
- }
5374
- else {
5375
- // Apply patch for non-object fields
5376
- result[key] = patch[key];
5377
- }
5363
+ close() {
5364
+ super.close();
5365
+ if (this._activePageChangedSub) {
5366
+ this._activePageChangedSub.unsubscribe();
5378
5367
  }
5379
- return result;
5380
5368
  }
5381
5369
  /***************************************************************************
5382
5370
  * *
5383
- * Private methods *
5371
+ * Private methods *
5372
+ * *
5373
+ **************************************************************************/
5374
+ clearAll() {
5375
+ super.clearAll();
5376
+ this.setActiveIndex(0);
5377
+ }
5378
+ reloadInternal() {
5379
+ if (this._activePageLoad) {
5380
+ // Cancel previous pending request
5381
+ this._activePageLoad.unsubscribe();
5382
+ }
5383
+ const subject = new Subject();
5384
+ this.onLoading();
5385
+ const page = this.pageSnapshot;
5386
+ const pageRequest = new Pageable(page.index, page.size, this.sort.sortsSnapshot);
5387
+ this._activePageLoad = this.dataSource
5388
+ .findAllPaged(pageRequest, this.filter.filtersSnapshot)
5389
+ .pipe(first())
5390
+ .subscribe((success) => {
5391
+ this.setTotal(success.totalElements);
5392
+ this.setData(success.content);
5393
+ subject.next(success);
5394
+ this.onIdle();
5395
+ }, (err) => {
5396
+ this.clearData();
5397
+ this.actlogger.error(this.id + ': Failed to query data', err);
5398
+ subject.error(err);
5399
+ this.onError(err);
5400
+ }, () => {
5401
+ this.onIdle();
5402
+ });
5403
+ return subject.pipe(first());
5404
+ }
5405
+ /***************************************************************************
5406
+ * *
5407
+ * Event handlers *
5384
5408
  * *
5385
5409
  **************************************************************************/
5386
- static mapReducePatch(obj, pobj) {
5387
- return Object.entries(obj).reduce((acc, [key, value]) => {
5388
- if (!(key in pobj)) {
5389
- return { ...acc, [key]: value }; // If key doesn't exist in pobj, copy the value from obj
5390
- }
5391
- if (typeof pobj[key] === 'object' && pobj[key] !== null && !Array.isArray(pobj[key])) {
5392
- // If it's an object (but not an array), recursively apply rmap
5393
- return { ...acc, [key]: this.mapReducePatch(obj[key], pobj[key]) };
5394
- }
5395
- else {
5396
- // Otherwise, copy the value from pobj
5397
- return { ...acc, [key]: pobj[key] };
5398
- }
5399
- }, {});
5410
+ /**
5411
+ * Occurs when the sorts property has changed.
5412
+ */
5413
+ onSortsChanged(sorts) {
5414
+ this.setActiveIndex(0);
5415
+ super.onSortsChanged(sorts);
5416
+ }
5417
+ /**
5418
+ * Occurs when the filtersSnapshot property has changed.
5419
+ */
5420
+ onFiltersChanged(filters) {
5421
+ this.setActiveIndex(0);
5422
+ super.onFiltersChanged(filters);
5400
5423
  }
5401
5424
  }
5402
5425
 
5403
- class DataContextSourceEventBinding extends DataContextLifeCycleBinding {
5426
+ class MatTableDataContextBindingBuilder {
5404
5427
  /***************************************************************************
5405
5428
  * *
5406
- * Constructor *
5429
+ * Static Builder *
5407
5430
  * *
5408
5431
  **************************************************************************/
5409
- constructor(dataContext, dataApi, reloadOnChanges) {
5410
- super(dataContext);
5411
- this.dataApi = dataApi;
5412
- this.reloadOnChanges = reloadOnChanges;
5413
- this.subscribe();
5432
+ static start(dataContext$) {
5433
+ return new MatTableDataContextBindingBuilder(dataContext$);
5434
+ }
5435
+ constructor(dataContext$) {
5436
+ /***************************************************************************
5437
+ * *
5438
+ * Fields *
5439
+ * *
5440
+ **************************************************************************/
5441
+ this.logger = LoggerFactory.getLogger(this.constructor.name);
5442
+ this._dataContext$ = dataContext$;
5414
5443
  }
5415
5444
  /***************************************************************************
5416
5445
  * *
5417
- * Internal Impl *
5446
+ * Public API *
5418
5447
  * *
5419
5448
  **************************************************************************/
5420
- subscribe() {
5421
- this._subscription = this.dataApi.dataChanged.subscribe((event) => this.handleDataChangeEvent(event));
5449
+ withSorts(sorts$) {
5450
+ this._sorts$ = sorts$;
5451
+ return this;
5452
+ }
5453
+ withPaginator(paginator$) {
5454
+ this._matPaginator$ = paginator$;
5455
+ return this;
5456
+ }
5457
+ withContinuator(continuator$) {
5458
+ this._continuator$ = continuator$;
5459
+ return this;
5460
+ }
5461
+ bindUntil(destroy$) {
5462
+ return new MatTableDataContextBinding(this._dataContext$, this._sorts$, this._matPaginator$, this._continuator$, destroy$);
5463
+ }
5464
+ }
5465
+ class MatTableDataContextBinding {
5466
+ /***************************************************************************
5467
+ * *
5468
+ * Constructor *
5469
+ * *
5470
+ **************************************************************************/
5471
+ constructor(_dataContext$, _matSorts$, _matPaginator$, _continuator$, destroy$) {
5472
+ this._dataContext$ = _dataContext$;
5473
+ this._matSorts$ = _matSorts$;
5474
+ this._matPaginator$ = _matPaginator$;
5475
+ this._continuator$ = _continuator$;
5476
+ /***************************************************************************
5477
+ * *
5478
+ * Fields *
5479
+ * *
5480
+ **************************************************************************/
5481
+ this.logger = LoggerFactory.getLogger(this.constructor.name);
5482
+ this.subscribeUntil(destroy$);
5422
5483
  }
5423
5484
  /***************************************************************************
5424
5485
  * *
5425
5486
  * Private methods *
5426
5487
  * *
5427
5488
  **************************************************************************/
5428
- handleDataChangeEvent(event) {
5429
- if (this.reloadOnChanges && this.isReloadDesirable(event)) {
5430
- this._dataContext.reload('DataSourceChangeEvent');
5431
- return;
5432
- }
5433
- // We might also be able to perform deletions locally, and avoid reload data
5434
- if (event.modified) {
5435
- this.updateExisting(event.modified);
5489
+ subscribeUntil(destroy$) {
5490
+ if (this._matSorts$) {
5491
+ this.bindMatSortsToDataContextUntil(this._matSorts$, destroy$);
5492
+ this.bindDataContextToMatSortsUntil(this._matSorts$, destroy$);
5436
5493
  }
5437
- if (event.patches) {
5438
- this.updateExisting(this.buildPatchUpdates(event.patches));
5494
+ if (this._matPaginator$) {
5495
+ this.bindPaginatorUntil(this._matPaginator$, destroy$);
5439
5496
  }
5440
- }
5441
- updateExisting(entities) {
5442
- if (entities.length > 0) {
5443
- this._dataContext.update(entities);
5497
+ if (this._continuator$) {
5498
+ this.bindContinuatorUntil(this._continuator$, destroy$);
5444
5499
  }
5445
5500
  }
5446
- // TODO: check logic and add correct return type (should it coerce to boolean?).
5447
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
5448
- isReloadDesirable(event) {
5449
- return event.unknownChanges || event.created || event.deletedIds;
5450
- }
5451
- buildPatchUpdates(patches) {
5452
- return patches
5453
- .map((patch) => {
5454
- const existing = this._dataContext.findById(patch.entityId);
5455
- if (existing) {
5456
- return DeepPartialPatcher.patchShallowCopy(existing, patch.patch);
5501
+ bindDataContextToMatSortsUntil(matSorts$, destroy$) {
5502
+ const dcSorts$ = this._dataContext$.pipe(filter((dc) => !!dc), switchMap$1((dc) => dc.sort.sorts));
5503
+ combineLatest([dcSorts$, matSorts$])
5504
+ .pipe(takeUntil(destroy$))
5505
+ .subscribe(([dcSorts, matSorts]) => {
5506
+ if (dcSorts.length >= 1) {
5507
+ // At least one sort active
5508
+ const sort = dcSorts[0];
5509
+ this.updateMatSorts(sort, matSorts);
5457
5510
  }
5458
5511
  else {
5459
- return null;
5512
+ // No sort active
5513
+ this.updateMatSorts(Sort.NONE, matSorts);
5460
5514
  }
5461
- })
5462
- .filter((update) => !!update);
5515
+ });
5463
5516
  }
5464
- }
5465
-
5466
- class DataSourceEntityPatch {
5467
- constructor(entityId, patch) {
5468
- this.entityId = entityId;
5469
- this.patch = patch;
5517
+ updateMatSorts(dcSort, matSorts) {
5518
+ const active = dcSort.prop;
5519
+ const direction = this.toMatDirection(dcSort.dir);
5520
+ matSorts.forEach((matSort) => this.updateMatSort(active, direction, matSort));
5470
5521
  }
5471
- }
5472
- /**
5473
- * Notifies about changes in a DataSource.
5474
- */
5475
- class DataSourceChangeEvent {
5476
- static unknownChanges() {
5477
- return new DataSourceChangeEvent(null, null, null, null);
5522
+ updateMatSort(activeProp, direction, matSort) {
5523
+ if (activeProp) {
5524
+ if (!matSort.sortables.has(activeProp)) {
5525
+ // The current sort property is not part of this MatSort.
5526
+ activeProp = Sort.NONE.prop; // Force no sort in this mat context
5527
+ }
5528
+ }
5529
+ if (matSort.active !== activeProp || matSort.direction !== direction) {
5530
+ // We do only update matSort when there was a real change
5531
+ matSort.active = activeProp;
5532
+ matSort.direction = direction;
5533
+ matSort._stateChanges.next();
5534
+ }
5478
5535
  }
5479
- static deleted(ids) {
5480
- return new DataSourceChangeEvent(ids, null, null, null);
5536
+ bindMatSortsToDataContextUntil(sorts$, destroy$) {
5537
+ const sortChanges$ = sorts$.pipe(mergeMap((sorts) => merge(...sorts.map((s) => s.sortChange))), map((matSort) => {
5538
+ return new Sort(matSort.active, this.fromMatDirection(matSort.direction));
5539
+ }));
5540
+ combineLatest([this._dataContext$, sortChanges$])
5541
+ .pipe(takeUntil(destroy$))
5542
+ .subscribe(([dc, sortRequest]) => {
5543
+ if (dc) {
5544
+ dc.sort.updateSort(sortRequest);
5545
+ }
5546
+ });
5481
5547
  }
5482
- static modified(modified) {
5483
- return new DataSourceChangeEvent(null, modified, null, null);
5548
+ bindPaginatorUntil(paginator$, destroy$) {
5549
+ const pageRequest$ = paginator$.pipe(filter((paginator) => !!paginator), switchMap$1((paginator) => paginator.page), map((pageEvent) => new PageRequest(pageEvent.pageIndex, pageEvent.pageSize)));
5550
+ combineLatest([this._dataContext$, pageRequest$])
5551
+ .pipe(takeUntil(destroy$))
5552
+ .subscribe(([dc, pageRequest]) => {
5553
+ if (dc) {
5554
+ if (isActivePagedDataContext(dc)) {
5555
+ const pagedDc = dc;
5556
+ pagedDc.setActivePage(pageRequest);
5557
+ }
5558
+ else {
5559
+ this.logger.warn('Can not bind the given paginator to the given data-context,' +
5560
+ ' as the datacontext does not support pagination!', dc);
5561
+ }
5562
+ }
5563
+ });
5484
5564
  }
5485
- static created(created) {
5486
- return new DataSourceChangeEvent(null, null, created, null);
5565
+ bindContinuatorUntil(continuator$, destroy$) {
5566
+ const chunkSizeChange$ = continuator$.pipe(filter((continuator) => !!continuator), switchMap$1((continuator) => outputToObservable(continuator.chunkSizeChange)));
5567
+ combineLatest([this._dataContext$, chunkSizeChange$])
5568
+ .pipe(takeUntil(destroy$))
5569
+ .subscribe(([dc, newChunkSize]) => {
5570
+ if (dc) {
5571
+ if (isContinuableDataContext(dc)) {
5572
+ const continuableDc = dc;
5573
+ continuableDc.chunkSize = newChunkSize;
5574
+ }
5575
+ else {
5576
+ this.logger.warn('Can not bind the given Continuator to the given data-context,' +
5577
+ ' as the DataContext does not support continuation!', dc);
5578
+ }
5579
+ }
5580
+ });
5487
5581
  }
5488
- static patched(patches) {
5489
- return new DataSourceChangeEvent(null, null, null, patches);
5582
+ toMatDirection(direction) {
5583
+ return direction;
5490
5584
  }
5491
- constructor(deletedIds, modified, created, patches) {
5492
- this.deletedIds = deletedIds;
5493
- this.modified = modified;
5494
- this.created = created;
5495
- this.patches = patches;
5496
- this.unknownChanges = !deletedIds && !modified && !created && !patches;
5585
+ fromMatDirection(matSortDirection) {
5586
+ return matSortDirection;
5497
5587
  }
5498
5588
  }
5499
5589
 
5500
- class EntityIdUtil {
5501
- static getId(entity, idProperty) {
5502
- if (entity && idProperty) {
5503
- if (typeof entity === 'object') {
5504
- return entity?.[idProperty];
5505
- }
5506
- }
5507
- return entity;
5508
- }
5509
- static extractIdFnOrProperty(idPropertyOrExtractFn) {
5510
- if (typeof idPropertyOrExtractFn === 'string') {
5511
- return EntityIdUtil.extractIdFn(idPropertyOrExtractFn);
5512
- }
5513
- else if (typeof idPropertyOrExtractFn === 'function') {
5514
- return idPropertyOrExtractFn;
5515
- }
5516
- else if (idPropertyOrExtractFn === null || idPropertyOrExtractFn === undefined) {
5517
- return EntityIdUtil.extractIdFn(null);
5518
- }
5519
- else {
5520
- throw new Error('Invalid idPropertyOrExtractFn');
5590
+ /**
5591
+ * Allows making any kind of subscription which will be automatically unsubscribed
5592
+ * upon data context closing/cleanup.
5593
+ */
5594
+ class DataContextLifeCycleBinding {
5595
+ /***************************************************************************
5596
+ * *
5597
+ * Constructor *
5598
+ * *
5599
+ **************************************************************************/
5600
+ constructor(_dataContext) {
5601
+ this._dataContext = _dataContext;
5602
+ // eslint-disable-next-line eqeqeq
5603
+ if (_dataContext == null) {
5604
+ throw new Error('dataContext must not be null!');
5521
5605
  }
5606
+ this._dataContext.data.subscribe({
5607
+ complete: () => this.unsubscribe(),
5608
+ });
5522
5609
  }
5523
- static extractIdFn(idProperty) {
5524
- return (entity) => EntityIdUtil.getId(entity, idProperty);
5610
+ /***************************************************************************
5611
+ * *
5612
+ * Public API *
5613
+ * *
5614
+ **************************************************************************/
5615
+ unsubscribe() {
5616
+ if (this._subscription) {
5617
+ this._subscription.unsubscribe();
5618
+ this._subscription = null;
5619
+ }
5525
5620
  }
5526
5621
  }
5527
5622
 
5528
- class DataSourceBase {
5623
+ class RequiredFilterContextChangedEvent {
5624
+ constructor(requiredFilters, currentFilters, isValid) {
5625
+ this.requiredFilters = requiredFilters;
5626
+ this.currentFilters = currentFilters;
5627
+ this.isValid = isValid;
5628
+ }
5629
+ }
5630
+ class RequiredFilterEvaluator {
5529
5631
  /***************************************************************************
5530
5632
  * *
5531
- * Constructor *
5633
+ * Fields *
5532
5634
  * *
5533
5635
  **************************************************************************/
5534
- constructor(propertyOrIdExtractor) {
5535
- /***************************************************************************
5536
- * *
5537
- * Fields *
5538
- * *
5539
- **************************************************************************/
5540
- this.dataChangeEvents$ = new Subject();
5541
- this.extractIdFn = EntityIdUtil.extractIdFnOrProperty(propertyOrIdExtractor);
5636
+ constructor(filterContext, requiredFilters) {
5637
+ this._requiredFilters = new BehaviorSubject([]);
5638
+ // eslint-disable-next-line eqeqeq
5639
+ if (filterContext == null) {
5640
+ throw new Error('filterContext must not be null!');
5641
+ }
5642
+ this._filterContext = filterContext;
5643
+ if (requiredFilters) {
5644
+ this._requiredFilters.next(requiredFilters);
5645
+ }
5646
+ this.context$ = combineLatest([this._requiredFilters, this._filterContext.filters]).pipe(map(([required, currentFilters]) => this.createEvent(requiredFilters, currentFilters)));
5542
5647
  }
5543
5648
  /***************************************************************************
5544
5649
  * *
5545
5650
  * Properties *
5546
5651
  * *
5547
5652
  **************************************************************************/
5548
- get dataChanged() {
5549
- return this.dataChangeEvents$.asObservable();
5653
+ get filterContext() {
5654
+ return this._filterContext;
5655
+ }
5656
+ get requiredFilters$() {
5657
+ return this._requiredFilters.asObservable();
5658
+ }
5659
+ get requiredFilters() {
5660
+ return this._requiredFilters.getValue();
5550
5661
  }
5551
5662
  /***************************************************************************
5552
5663
  * *
5553
5664
  * Public API *
5554
5665
  * *
5555
5666
  **************************************************************************/
5556
- publishChangeEvent(e) {
5557
- this.dataChangeEvents$.next(e);
5558
- }
5559
- getId(entity) {
5560
- return this.extractIdFn(entity);
5561
- }
5562
- }
5563
-
5564
- class SortUtil {
5565
- static toggleDir(dir) {
5566
- if (dir === 'asc') {
5567
- return 'desc';
5568
- }
5569
- else {
5570
- return 'asc';
5571
- }
5572
- }
5573
- static toggleSort(sort) {
5574
- return new Sort(sort.prop, SortUtil.toggleDir(sort.dir));
5575
- }
5576
- static sortData(data, sorts, prefix) {
5577
- if (sorts && sorts.length > 0) {
5578
- const copy = [...data];
5579
- const sortFields = sorts.map((s) => (s.dir === 'desc' ? '-' : '') + SortUtil.propertyPath(s, prefix));
5580
- return copy.sort(ComparatorBuilder.fieldSort(...sortFields));
5581
- }
5582
- else {
5583
- return data;
5584
- }
5667
+ /***************************************************************************
5668
+ * *
5669
+ * Private methods *
5670
+ * *
5671
+ **************************************************************************/
5672
+ createEvent(requiredFilters, currentFilters) {
5673
+ return new RequiredFilterContextChangedEvent(requiredFilters, currentFilters, this.allRequiredFiltersPresent(requiredFilters, currentFilters));
5585
5674
  }
5586
- /**
5587
- * Checks if the two arrays have all content references in the exact same order.
5588
- * @param data
5589
- * @param other
5590
- */
5591
- static equalsExactRefs(data, other) {
5592
- // eslint-disable-next-line eqeqeq
5593
- if (data.length == other.length) {
5594
- for (let i = 0; i < data.length; i++) {
5595
- // eslint-disable-next-line eqeqeq
5596
- if (data[i] != other[i]) {
5597
- return false;
5598
- }
5599
- }
5600
- return true;
5675
+ allRequiredFiltersPresent(requiredFilters, currentFilters) {
5676
+ if (requiredFilters) {
5677
+ return requiredFilters.some((filterGroup) => this.hasAllFilters(filterGroup, currentFilters));
5601
5678
  }
5602
- return false;
5679
+ return true;
5603
5680
  }
5604
- static propertyPath(sort, prefix) {
5605
- if (prefix) {
5606
- return prefix + '.' + sort.prop;
5607
- }
5608
- else {
5609
- return sort.prop;
5610
- }
5681
+ hasAllFilters(requiredFilters, currentFilters) {
5682
+ const requiredFilterSet = new Set(requiredFilters);
5683
+ const currentFilterKeySet = new Set(currentFilters.map((f) => f.key));
5684
+ const areAllRequiredFiltersPresent = requiredFilters.every((required) => currentFilterKeySet.has(required));
5685
+ const areAllRequiredFiltersHaveValue = currentFilters
5686
+ .filter((f) => requiredFilterSet.has(f.key))
5687
+ .every((currentFilter) => Filter.hasValue(currentFilter));
5688
+ return areAllRequiredFiltersPresent && areAllRequiredFiltersHaveValue;
5611
5689
  }
5612
5690
  }
5613
5691
 
5614
- class MapUtils {
5615
- static toDistinctMap(values, keyFn) {
5616
- return values.reduce((map, value) => {
5617
- map.set(keyFn(value), value);
5618
- return map;
5619
- }, new Map());
5692
+ class AutoStartSpec {
5693
+ static asap(sort) {
5694
+ return new AutoStartSpec(null, sort);
5620
5695
  }
5621
- static groupByKey(values, keyFn) {
5622
- const groups = new Map();
5623
- values.forEach((value) => {
5624
- const key = keyFn(value);
5625
- let group = groups.get(key);
5626
- if (!group) {
5627
- group = [];
5628
- groups.set(key, group);
5629
- }
5630
- group.push(value);
5631
- });
5632
- return groups;
5696
+ static requireFiltersAll(filters, sort) {
5697
+ return AutoStartSpec.requireFilters([filters], sort);
5633
5698
  }
5634
- static mapValue(map, valueMapFn) {
5635
- const newMap = new Map();
5636
- Array.from(map.entries()).forEach(([key, value]) => newMap.set(key, valueMapFn(value)));
5637
- return newMap;
5699
+ static requireFiltersAny(filters, sort) {
5700
+ const separateFilterArrays = filters.map((singleFilter) => [singleFilter]);
5701
+ return AutoStartSpec.requireFilters(separateFilterArrays, sort);
5638
5702
  }
5639
- }
5640
-
5641
- class LocalListDataSource extends DataSourceBase {
5642
- /***************************************************************************
5643
- * *
5644
- * Static Builder *
5645
- * *
5646
- **************************************************************************/
5647
- /**
5648
- * Creates an empty local list data-source.
5649
- * You can set / modify data by using the data property.
5650
- * @param idPropertyOrExtractor
5651
- * @param localSort
5652
- * @param localFilter
5653
- */
5654
- static empty(idPropertyOrExtractor, localSort, localFilter) {
5655
- return this.from([], idPropertyOrExtractor, localSort, localFilter);
5703
+ static requireFilters(filters, sort) {
5704
+ return new AutoStartSpec(filters, sort);
5656
5705
  }
5657
- static from(localData, idPropertyOrExtractor, localSort, localFilter) {
5658
- if (idPropertyOrExtractor === null) {
5659
- idPropertyOrExtractor = LocalListDataSource.guessIdProperty(localData);
5660
- }
5661
- return new LocalListDataSource(localData, localSort, localFilter, idPropertyOrExtractor);
5706
+ constructor(requiredFilters, initialSort) {
5707
+ this.requiredFilters = requiredFilters;
5708
+ this.initialSort = initialSort;
5662
5709
  }
5710
+ }
5711
+ class DataContextAutoStarter extends DataContextLifeCycleBinding {
5663
5712
  /***************************************************************************
5664
5713
  * *
5665
5714
  * Constructor *
5666
5715
  * *
5667
5716
  **************************************************************************/
5668
- constructor(localData, localSort, localFilter, idPropertyOrExtractor) {
5669
- super(idPropertyOrExtractor);
5717
+ constructor(dataContext, _autoStartSpec) {
5718
+ super(dataContext);
5719
+ this._autoStartSpec = _autoStartSpec;
5670
5720
  /***************************************************************************
5671
5721
  * *
5672
5722
  * Fields *
5673
5723
  * *
5674
5724
  **************************************************************************/
5675
5725
  this.logger = LoggerFactory.getLogger(this.constructor.name);
5676
- this.data$ = new BehaviorSubject([]);
5677
- if (!localData) {
5678
- throw new Error('localData must not be null!');
5726
+ // eslint-disable-next-line eqeqeq
5727
+ if (_autoStartSpec == null) {
5728
+ throw new Error('autoStartSpec must not be null!');
5679
5729
  }
5680
- this.localSort = localSort || SortUtil.sortData;
5681
- this.localFilter = localFilter || FilterUtil.filterData;
5682
- this.data = localData;
5730
+ if (_autoStartSpec.requiredFilters) {
5731
+ this._autoStartConditionFulfilled$ = this.buildRequiredFilterConditionObservable();
5732
+ }
5733
+ else {
5734
+ // no condition defined, try to start immediately
5735
+ this.startDataContext();
5736
+ }
5737
+ this.subscribe();
5683
5738
  }
5684
5739
  /***************************************************************************
5685
5740
  * *
5686
- * Properties *
5741
+ * Public API *
5687
5742
  * *
5688
5743
  **************************************************************************/
5689
- get data() {
5690
- return this.data$.getValue();
5691
- }
5692
- set data(data) {
5693
- this.replaceAll(data);
5744
+ subscribe() {
5745
+ const hasRequiredFilters = !!this._autoStartConditionFulfilled$;
5746
+ if (hasRequiredFilters) {
5747
+ const startedTrue$ = this._dataContext.isStarted$.pipe(filter((t) => t));
5748
+ this._subscription = this._autoStartConditionFulfilled$
5749
+ .pipe(takeUntil(startedTrue$), tap((fulfilled) => this.logger.debug(`Got fulfilled event: ${fulfilled}`)), filter((fulfilled) => !!fulfilled))
5750
+ .subscribe(() => this.startDataContext());
5751
+ }
5694
5752
  }
5695
5753
  /***************************************************************************
5696
5754
  * *
5697
- * IDataSource API *
5755
+ * Private methods *
5698
5756
  * *
5699
5757
  **************************************************************************/
5700
- findById(id) {
5701
- if (id === undefined || id === null) {
5702
- throw new Error('findById: id argument required!');
5703
- }
5704
- const found = this.data.find((d) => this.getId(d) === id);
5705
- if (found) {
5706
- return of(found);
5707
- }
5708
- else {
5709
- return throwError(() => new Error("Could not find local entity by id: '" + id + "'"));
5710
- }
5711
- }
5712
- findByIds(ids) {
5713
- if (ids === undefined || ids === null) {
5714
- throw new Error('findByIds: ids array argument required!');
5715
- }
5716
- const desiredIds = new Set(ids);
5717
- return of(this.data.filter((d) => desiredIds.has(this.getId(d))));
5758
+ buildRequiredFilterConditionObservable() {
5759
+ return new RequiredFilterEvaluator(this._dataContext.filter, this._autoStartSpec.requiredFilters).context$.pipe(map((event) => event.isValid));
5718
5760
  }
5719
- findAllFiltered(filters, sorts) {
5720
- return of(this.data).pipe(map((data) => this.localFilter(data, filters)), map((data) => this.localSort(data, sorts)));
5761
+ startDataContext() {
5762
+ this.logger.debug(this._dataContext.id + ': Auto starting ...');
5763
+ this._dataContext.start(this._autoStartSpec.initialSort);
5721
5764
  }
5765
+ }
5766
+
5767
+ class DeepPartialPatcher {
5722
5768
  /***************************************************************************
5723
5769
  * *
5724
5770
  * Public API *
5725
5771
  * *
5726
5772
  **************************************************************************/
5727
- replaceAll(data) {
5728
- this.silentReplaceData(data);
5729
- this.publishChangeEvent(DataSourceChangeEvent.unknownChanges());
5730
- }
5731
- delete(entity) {
5732
- this.deleteAll([entity]);
5733
- }
5734
- deleteAll(toDelete) {
5735
- if (toDelete?.length > 0) {
5736
- return this.deleteAllById(toDelete.map((e) => this.getId(e)));
5737
- }
5738
- }
5739
- deleteAllById(idsToDelete) {
5740
- if (idsToDelete?.length > 0) {
5741
- const existing = this.data;
5742
- const idsToDeleteSet = new Set(idsToDelete);
5743
- this.silentReplaceData(existing.filter((e) => !idsToDeleteSet.has(this.getId(e))));
5744
- this.publishChangeEvent(DataSourceChangeEvent.deleted(idsToDelete));
5745
- }
5746
- }
5747
- saveAll(toSave) {
5748
- const idDataMap = this.buildIdDataMap(this.data);
5749
- const createdEntities = [];
5750
- const modifiedEntities = [];
5751
- toSave.forEach((entity) => {
5752
- const id = this.getId(entity);
5753
- if (!idDataMap.has(id)) {
5754
- createdEntities.push(entity);
5755
- }
5756
- else {
5757
- modifiedEntities.push(entity);
5758
- }
5759
- idDataMap.set(id, entity);
5760
- });
5761
- this.silentReplaceData(Array.from(idDataMap.values()));
5762
- this.publishChanges(createdEntities, modifiedEntities);
5773
+ /**
5774
+ * All modified parts are deep cloned so the original is never modified.
5775
+ */
5776
+ static patchDeepClone(original, patch) {
5777
+ return this.mapReducePatch(original, patch);
5763
5778
  }
5764
- save(entity, index) {
5765
- this.saveAtIndex(entity, index);
5779
+ /**
5780
+ * Returns a shallow copy of the original, but may modify nested objects.
5781
+ */
5782
+ static patchShallowCopy(original, patch) {
5783
+ const shallowCopy = { ...original };
5784
+ return this.patchOriginal(shallowCopy, patch);
5766
5785
  }
5767
- saveAtIndex(entity, index) {
5768
- const id = this.getId(entity);
5769
- const newData = [...this.data];
5770
- const existingIndex = newData.findIndex((entity) => this.getId(entity) === id);
5771
- let created = false;
5772
- if (existingIndex === -1) {
5773
- if (index !== null) {
5774
- newData.splice(index, 0, entity);
5786
+ /**
5787
+ * Modifies the original directly. Returns a reference to the original object.
5788
+ */
5789
+ static patchOriginal(original, patch) {
5790
+ const result = original;
5791
+ for (const key in patch) {
5792
+ if (patch[key] && typeof patch[key] === 'object' && !Array.isArray(patch[key])) {
5793
+ // Recursively apply patch for nested objects
5794
+ result[key] = DeepPartialPatcher.patchOriginal(result[key] ?? new Object(patch[key]), patch[key]);
5775
5795
  }
5776
5796
  else {
5777
- newData.push(entity);
5797
+ // Apply patch for non-object fields
5798
+ result[key] = patch[key];
5778
5799
  }
5779
- created = true;
5780
- }
5781
- else {
5782
- newData[existingIndex] = entity;
5783
5800
  }
5784
- this.silentReplaceData(newData);
5785
- this.publishChangeEvent(created ? DataSourceChangeEvent.created([entity]) : DataSourceChangeEvent.modified([entity]));
5801
+ return result;
5786
5802
  }
5787
5803
  /***************************************************************************
5788
5804
  * *
5789
5805
  * Private methods *
5790
5806
  * *
5791
5807
  **************************************************************************/
5792
- buildIdDataMap(data) {
5793
- return MapUtils.toDistinctMap(data, (value) => this.getId(value));
5794
- }
5795
- silentReplaceData(newData) {
5796
- this.data$.next(newData);
5797
- }
5798
- static guessIdProperty(localData) {
5799
- const log = LoggerFactory.getLogger('LocalListDataSource');
5800
- if (localData && localData.length > 0) {
5801
- const sample = localData[0];
5802
- if (typeof sample === 'object') {
5803
- if (Object.prototype.hasOwnProperty.call(sample, 'id')) {
5804
- log.warn('DataSource without defined id-property => autodetected property id-property as "id"');
5805
- return 'id'; // Use id
5806
- }
5807
- else {
5808
- log.warn('Local DataSource created without defined id-property and objects. Using object equality!');
5809
- }
5808
+ static mapReducePatch(obj, pobj) {
5809
+ return Object.entries(obj).reduce((acc, [key, value]) => {
5810
+ if (!(key in pobj)) {
5811
+ return { ...acc, [key]: value }; // If key doesn't exist in pobj, copy the value from obj
5810
5812
  }
5811
- }
5812
- return null; // Use value as id if scalar
5813
- }
5814
- publishChanges(createdEntities, modifiedEntities) {
5815
- if (createdEntities.length > 0) {
5816
- this.publishChangeEvent(DataSourceChangeEvent.created(createdEntities));
5817
- }
5818
- if (modifiedEntities.length > 0) {
5819
- this.publishChangeEvent(DataSourceChangeEvent.modified(modifiedEntities));
5820
- }
5813
+ if (typeof pobj[key] === 'object' && pobj[key] !== null && !Array.isArray(pobj[key])) {
5814
+ // If it's an object (but not an array), recursively apply rmap
5815
+ return { ...acc, [key]: this.mapReducePatch(obj[key], pobj[key]) };
5816
+ }
5817
+ else {
5818
+ // Otherwise, copy the value from pobj
5819
+ return { ...acc, [key]: pobj[key] };
5820
+ }
5821
+ }, {});
5821
5822
  }
5822
5823
  }
5823
5824
 
5824
- class LocalPagedDataSource {
5825
- /***************************************************************************
5826
- * *
5827
- * Static Builder *
5828
- * *
5829
- **************************************************************************/
5830
- /**
5831
- * Creates an empty local list data-source.
5832
- * You can set / modify data by using the data property.
5833
- * @param idProperty
5834
- * @param localSort
5835
- * @param localFilter
5836
- */
5837
- static empty(idProperty, localSort, localFilter) {
5838
- return LocalPagedDataSource.of(LocalListDataSource.empty(idProperty, localSort, localFilter));
5839
- }
5840
- static of(listDataSource) {
5841
- return new LocalPagedDataSource(listDataSource);
5842
- }
5825
+ class DataContextSourceEventBinding extends DataContextLifeCycleBinding {
5843
5826
  /***************************************************************************
5844
5827
  * *
5845
5828
  * Constructor *
5846
5829
  * *
5847
5830
  **************************************************************************/
5848
- constructor(listDataSource) {
5849
- if (!listDataSource) {
5850
- throw new Error('listDataSource must not be null!');
5851
- }
5852
- this.localListFetcher = listDataSource;
5831
+ constructor(dataContext, dataApi, reloadOnChanges) {
5832
+ super(dataContext);
5833
+ this.dataApi = dataApi;
5834
+ this.reloadOnChanges = reloadOnChanges;
5835
+ this.subscribe();
5853
5836
  }
5854
5837
  /***************************************************************************
5855
5838
  * *
5856
- * Public API *
5839
+ * Internal Impl *
5857
5840
  * *
5858
5841
  **************************************************************************/
5859
- get dataChanged() {
5860
- return this.localListFetcher.dataChanged;
5861
- }
5862
- findById(id) {
5863
- return this.localListFetcher.findById(id);
5864
- }
5865
- findByIds(ids) {
5866
- return this.localListFetcher.findByIds(ids);
5867
- }
5868
- findAllPaged(pageable, filters) {
5869
- return this.localListFetcher
5870
- .findAllFiltered(filters, pageable.sorts)
5871
- .pipe(map((data) => this.pageSlice(pageable, data)));
5872
- }
5873
- getId(entity) {
5874
- return this.localListFetcher.getId(entity);
5842
+ subscribe() {
5843
+ this._subscription = this.dataApi.dataChanged.subscribe((event) => this.handleDataChangeEvent(event));
5875
5844
  }
5876
5845
  /***************************************************************************
5877
5846
  * *
5878
5847
  * Private methods *
5879
5848
  * *
5880
5849
  **************************************************************************/
5881
- pageSlice(pageable, data) {
5882
- let page;
5883
- if (data) {
5884
- const start = pageable.page * pageable.size;
5885
- const end = start + pageable.size;
5886
- const slice = data.slice(start, end);
5887
- page = Page.fromPage(slice, data.length, pageable);
5850
+ handleDataChangeEvent(event) {
5851
+ if (this.reloadOnChanges && this.isReloadDesirable(event)) {
5852
+ this._dataContext.reload('DataSourceChangeEvent');
5853
+ return;
5888
5854
  }
5889
- else {
5890
- page = Page.empty();
5855
+ // We might also be able to perform deletions locally, and avoid reload data
5856
+ if (event.modified) {
5857
+ this.updateExisting(event.modified);
5891
5858
  }
5892
- return page;
5859
+ if (event.patches) {
5860
+ this.updateExisting(this.buildPatchUpdates(event.patches));
5861
+ }
5862
+ }
5863
+ updateExisting(entities) {
5864
+ if (entities.length > 0) {
5865
+ this._dataContext.update(entities);
5866
+ }
5867
+ }
5868
+ // TODO: check logic and add correct return type (should it coerce to boolean?).
5869
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
5870
+ isReloadDesirable(event) {
5871
+ return event.unknownChanges || event.created || event.deletedIds;
5872
+ }
5873
+ buildPatchUpdates(patches) {
5874
+ return patches
5875
+ .map((patch) => {
5876
+ const existing = this._dataContext.findById(patch.entityId);
5877
+ if (existing) {
5878
+ return DeepPartialPatcher.patchShallowCopy(existing, patch.patch);
5879
+ }
5880
+ else {
5881
+ return null;
5882
+ }
5883
+ })
5884
+ .filter((update) => !!update);
5893
5885
  }
5894
5886
  }
5895
5887
 
@@ -23630,6 +23622,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
23630
23622
  args: [ElderSelectOptionComponent]
23631
23623
  }] } });
23632
23624
 
23625
+ const DEFAULT_DEBOUNCE_TIME = 150;
23633
23626
  class ElderAutocompleteDirective {
23634
23627
  /***************************************************************************
23635
23628
  * *
@@ -23644,11 +23637,10 @@ class ElderAutocompleteDirective {
23644
23637
  * *
23645
23638
  **************************************************************************/
23646
23639
  this.logger = LoggerFactory.getLogger(this.constructor.name);
23647
- this.ignoreKeys = ['ArrowRight', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'Tab'];
23648
- this.inputKeyup$ = new Subject();
23649
23640
  this.queryFilter = 'query';
23650
23641
  this.filters = [];
23651
23642
  this.sorts = [];
23643
+ this.userTextInputChanged$ = new Subject();
23652
23644
  this.destroyMatAutoBinding$ = new Subject();
23653
23645
  this.destroy$ = new Subject();
23654
23646
  }
@@ -23657,11 +23649,9 @@ class ElderAutocompleteDirective {
23657
23649
  * Host Listener *
23658
23650
  * *
23659
23651
  **************************************************************************/
23660
- onKeyUp(event) {
23661
- if (this.ignoreKeys.some((k) => k === event.key)) {
23662
- return;
23663
- }
23664
- this.inputKeyup$.next(event);
23652
+ onUserTextInput(event) {
23653
+ const value = event?.target?.value ?? '';
23654
+ this.userTextInputChanged$.next(value);
23665
23655
  }
23666
23656
  /***************************************************************************
23667
23657
  * *
@@ -23669,7 +23659,7 @@ class ElderAutocompleteDirective {
23669
23659
  * *
23670
23660
  **************************************************************************/
23671
23661
  ngOnInit() {
23672
- merge(this.inputKeyup$.pipe(skipWhile((e) => !this._elderAutocomplete.enabled), debounce(() => timer(this.getDebounceTime())), map((e) => e?.target?.value)), this.autocomplete.triggerReload$)
23662
+ merge(this.userTextInputChanged$.pipe(filter(() => this._elderAutocomplete.enabled), debounce(() => timer(this.getDebounceTime())), distinctUntilChanged()), this.autocomplete.triggerReload$)
23673
23663
  .pipe(takeUntil(this.destroy$), filter((value) => !value || typeof value === 'string' || typeof value === 'number'))
23674
23664
  .subscribe((value) => this.updateSuggestions(value));
23675
23665
  }
@@ -23723,10 +23713,10 @@ class ElderAutocompleteDirective {
23723
23713
  if (isLocalDataSource(this.dataContext?.dataSource)) {
23724
23714
  return 0;
23725
23715
  }
23726
- return 150; // Default debounce time
23716
+ return DEFAULT_DEBOUNCE_TIME;
23727
23717
  }
23728
23718
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ElderAutocompleteDirective, deps: [{ token: i1$8.MatAutocompleteTrigger }], target: i0.ɵɵFactoryTarget.Directive }); }
23729
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: ElderAutocompleteDirective, isStandalone: true, selector: "[elderAutocomplete]", inputs: { queryFilter: "queryFilter", filters: "filters", sorts: "sorts", autocomplete: ["elderAutocomplete", "autocomplete"] }, host: { listeners: { "keyup": "onKeyUp($event)" } }, ngImport: i0 }); }
23719
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: ElderAutocompleteDirective, isStandalone: true, selector: "[elderAutocomplete]", inputs: { queryFilter: "queryFilter", filters: "filters", sorts: "sorts", autocomplete: ["elderAutocomplete", "autocomplete"] }, host: { listeners: { "input": "onUserTextInput($event)" } }, ngImport: i0 }); }
23730
23720
  }
23731
23721
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ElderAutocompleteDirective, decorators: [{
23732
23722
  type: Directive,
@@ -23739,9 +23729,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
23739
23729
  type: Input
23740
23730
  }], sorts: [{
23741
23731
  type: Input
23742
- }], onKeyUp: [{
23732
+ }], onUserTextInput: [{
23743
23733
  type: HostListener,
23744
- args: ['keyup', ['$event']]
23734
+ args: ['input', ['$event']]
23745
23735
  }], autocomplete: [{
23746
23736
  type: Input,
23747
23737
  args: ['elderAutocomplete']
@@ -23833,25 +23823,25 @@ class ElderSelectOnTabDirective {
23833
23823
  selectActiveOption() {
23834
23824
  // keyboard navigation has precedence over text input
23835
23825
  if (this.panelNavigatedWithArrowKeys) {
23836
- const activeOption = this.autoTrigger.activeOption;
23837
- if (activeOption) {
23838
- const entity = activeOption.value;
23839
- this.writeEntity(entity);
23840
- }
23826
+ this.writeAutoCompleteActiveOption();
23841
23827
  return;
23842
23828
  }
23843
- // text input check
23844
- if (this.textTypedAndSettingRespected()) {
23845
- const matchedOption = this.findMatchingOptionByInputText();
23846
- if (matchedOption) {
23847
- this.writeEntity(matchedOption.value);
23848
- return;
23849
- }
23850
- const activeOption = this.autoTrigger.activeOption;
23851
- if (activeOption) {
23852
- const entity = activeOption.value;
23853
- this.writeEntity(entity);
23854
- }
23829
+ // only select if text was typed and the setting is respected
23830
+ if (!this.textTypedAndSettingRespected()) {
23831
+ return;
23832
+ }
23833
+ const matchedOption = this.findMatchingOptionByInputText();
23834
+ if (matchedOption) {
23835
+ this.writeEntity(matchedOption.value);
23836
+ return;
23837
+ }
23838
+ this.writeAutoCompleteActiveOption();
23839
+ }
23840
+ writeAutoCompleteActiveOption() {
23841
+ const activeOption = this.autoTrigger.activeOption;
23842
+ if (activeOption) {
23843
+ const entity = activeOption.value;
23844
+ this.writeEntity(entity);
23855
23845
  }
23856
23846
  }
23857
23847
  writeEntity(entity) {
@@ -23881,20 +23871,24 @@ class ElderSelectOnTabDirective {
23881
23871
  findMatchingOptionByInputText() {
23882
23872
  const inputText = this.elderSelect.inputRef?.nativeElement?.value;
23883
23873
  if (!inputText) {
23884
- return null;
23874
+ return undefined;
23885
23875
  }
23886
23876
  const normalizedInput = inputText.trim().toLowerCase();
23887
23877
  if (!normalizedInput) {
23888
- return null;
23878
+ return undefined;
23889
23879
  }
23890
23880
  const options = this.autoTrigger.autocomplete?.options?.toArray() ?? [];
23891
- return (options.find((option) => {
23881
+ return this.findFirstOptionMatchingNormalizedInput(options, normalizedInput);
23882
+ }
23883
+ findFirstOptionMatchingNormalizedInput(options, normalizedInput) {
23884
+ return options.find((option) => {
23892
23885
  if (option.disabled) {
23893
23886
  return false;
23894
23887
  }
23895
- const optionText = (option.viewValue ?? '').trim().toLowerCase();
23896
- return optionText.startsWith(normalizedInput);
23897
- }) ?? null);
23888
+ const optionText = option.viewValue ?? '';
23889
+ const normalizedOptionText = optionText.trim().toLowerCase();
23890
+ return normalizedOptionText.startsWith(normalizedInput);
23891
+ });
23898
23892
  }
23899
23893
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ElderSelectOnTabDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
23900
23894
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: ElderSelectOnTabDirective, isStandalone: true, selector: "[elderSelectOnTab]", host: { listeners: { "keydown.arrowup": "handleVerticalArrowKeyPress()", "keydown.arrowdown": "handleVerticalArrowKeyPress()", "input": "handleInputTyping()", "keydown.tab": "handleTabKeyPress()" } }, ngImport: i0 }); }
@@ -24073,19 +24067,13 @@ class ElderSuggestionPanelComponent {
24073
24067
  if (!activeEntity) {
24074
24068
  return false;
24075
24069
  }
24076
- // Prefer stable id comparison for object entities.
24077
- const activeId = this.safeId(activeEntity);
24078
- const optionId = this.safeId(option);
24079
- if (activeId !== undefined && optionId !== undefined) {
24080
- return activeId === optionId;
24070
+ const areIdsEqual = this.areIdsEqual(activeEntity, option);
24071
+ if (areIdsEqual !== undefined) {
24072
+ return areIdsEqual;
24073
+ }
24074
+ else {
24075
+ return this.areLabelsEqual(activeEntity, option);
24081
24076
  }
24082
- // Fallback for non-id data (e.g. translated display-only options).
24083
- const resolver = this.displayPropertyResolver$.getValue();
24084
- const activeLabel = resolver
24085
- ? resolver(activeEntity)
24086
- : this.propertyStringValue(activeEntity, null);
24087
- const optionLabel = resolver ? resolver(option) : this.propertyStringValue(option, null);
24088
- return activeLabel === optionLabel;
24089
24077
  }
24090
24078
  toOptionValue(option) {
24091
24079
  if (this.optionValueConverterFn) {
@@ -24121,6 +24109,10 @@ class ElderSuggestionPanelComponent {
24121
24109
  calculatePageSize() {
24122
24110
  return this.PAGE_SIZE + this.hiddenOptionsCount$.getValue();
24123
24111
  }
24112
+ /**
24113
+ * Try to extract the id from the value (to determine active entity)
24114
+ * with graceful fallback to undefined.
24115
+ */
24124
24116
  safeId(value) {
24125
24117
  if (value === null || value === undefined) {
24126
24118
  return undefined;
@@ -24129,11 +24121,27 @@ class ElderSuggestionPanelComponent {
24129
24121
  return this.getId(value);
24130
24122
  }
24131
24123
  catch {
24124
+ this.logger.debug('Failed to extract id from value', value);
24125
+ return undefined;
24126
+ }
24127
+ }
24128
+ areIdsEqual(activeEntity, option) {
24129
+ const activeId = this.safeId(activeEntity);
24130
+ const optionId = this.safeId(option);
24131
+ if ([activeId, optionId].includes(undefined)) {
24132
24132
  return undefined;
24133
24133
  }
24134
+ return activeId === optionId;
24135
+ }
24136
+ areLabelsEqual(activeEntity, option) {
24137
+ const resolverFn = this.displayPropertyResolver$.getValue();
24138
+ if (resolverFn) {
24139
+ return resolverFn(activeEntity) === resolverFn(option);
24140
+ }
24141
+ return this.propertyStringValue(activeEntity, null) === this.propertyStringValue(option, null);
24134
24142
  }
24135
24143
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ElderSuggestionPanelComponent, deps: [{ token: i0.NgZone }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component }); }
24136
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ElderSuggestionPanelComponent, isStandalone: true, selector: "elder-suggestion-panel", inputs: { isOptionDisabledFn: { classPropertyName: "isOptionDisabledFn", publicName: "isOptionDisabledFn", isSignal: false, isRequired: false, transformFunction: null }, isOptionHiddenFn: { classPropertyName: "isOptionHiddenFn", publicName: "isOptionHiddenFn", isSignal: false, isRequired: false, transformFunction: null }, optionValueConverterFn: { classPropertyName: "optionValueConverterFn", publicName: "optionValueConverterFn", isSignal: false, isRequired: false, transformFunction: null }, activeEntity: { classPropertyName: "activeEntity", publicName: "activeEntity", isSignal: true, isRequired: false, transformFunction: null }, enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: false, isRequired: false, transformFunction: null }, valueTemplate: { classPropertyName: "valueTemplate", publicName: "valueTemplate", isSignal: false, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: false, isRequired: false, transformFunction: null }, displayPropertyResolver: { classPropertyName: "displayPropertyResolver", publicName: "displayPropertyResolver", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected" }, queries: [{ propertyName: "valueTemplateQuery", first: true, predicate: ElderSelectValueDirective, descendants: true, read: TemplateRef, static: true }], viewQueries: [{ propertyName: "matAutocomplete", first: true, predicate: ["auto"], descendants: true }], exportAs: ["elderSuggestionPanel"], ngImport: i0, template: "<mat-autocomplete\n #auto=\"matAutocomplete\"\n panelWidth=\"auto\"\n [autoActiveFirstOption]=\"true\"\n (opened)=\"onAutocompleteOpened($event)\"\n (optionSelected)=\"onOptionSelected($event)\"\n elderInfiniteScroll\n elderElderInfiniteAutocomplete\n (closeToEnd)=\"onAutoCompleteCloseToEnd()\"\n>\n @if (dataContext$ | async; as dc) {\n @if (dc.isClosed) {\n <mat-option disabled>\n <div class=\"layout-row place-start-center gap-sm\">\n <mat-icon color=\"warn\">warning</mat-icon>\n <span class=\"mat-caption\">DataContext Closed!</span>\n </div>\n </mat-option>\n }\n\n @if (availableSuggestions$ | async; as suggestions) {\n @if (suggestions.length === 0) {\n <mat-option disabled>No Data.</mat-option>\n }\n\n @for (suggestion of suggestions; track getIdAsString(suggestion)) {\n @if (isOptionVisible(suggestion)) {\n <mat-option\n [value]=\"toOptionValue(suggestion)\"\n [id]=\"getIdAsString(suggestion)\"\n [disabled]=\"!isOptionAvailable(suggestion)\"\n [class.active-option]=\"isOptionActive(suggestion)\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate || simpleValueTemplate;\n context: { $implicit: suggestion }\n \"\n >\n </ng-container>\n <!--\n <span class=\"mat-caption\">value: {{toOptionValue(suggestion)}}</span>\n -->\n </mat-option>\n }\n }\n }\n } @else {\n <mat-option disabled>\n <span class=\"mat-caption\">\n No DataContext!\n @if (dataSource$ | async; as ds) {\n (DataSource: {{ ds ? 'available' : 'missing' }})\n {{ enabled ? 'Autocomplete Enabled' : 'Autocomplete DISABLED' }}\n }\n </span>\n </mat-option>\n }\n\n @if (dataState$ | async; as state) {\n @if (!state.idle || state.loading) {\n <div style=\"position: relative\">\n <div style=\"position: absolute; right: 0; bottom: -8px; left: 0\">\n <mat-progress-bar\n [value]=\"100\"\n [mode]=\"state.loading ? 'query' : 'determinate'\"\n [color]=\"state.error ? 'warn' : 'primary'\"\n ></mat-progress-bar>\n </div>\n </div>\n }\n }\n</mat-autocomplete>\n\n<ng-template #simpleValueTemplate let-value>\n @if (displayPropertyResolver$ | async; as propertyResolver) {\n <span class=\"noselect\">{{ propertyResolver(value) }}</span>\n }\n</ng-template>\n", styles: [".active-option.active-option{font-weight:700}.active-option.active-option:before{font-family:Material Icons;content:\"check\";font-size:13px;position:absolute;top:50%;left:1px;transform:translateY(-50%)}\n"], dependencies: [{ kind: "component", type: MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: ElderInfiniteAutocompleteDirective, selector: "mat-autocomplete[elderElderInfiniteAutocomplete]" }, { kind: "directive", type: ElderInfiniteScrollDirective, selector: "[elderInfiniteScroll]", inputs: ["listenToHost", "eventThrottle", "offsetFactor", "ignoreScrollEvent", "containerId", "scrollContainer"], outputs: ["closeToEnd", "scrolling"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24144
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ElderSuggestionPanelComponent, isStandalone: true, selector: "elder-suggestion-panel", inputs: { isOptionDisabledFn: { classPropertyName: "isOptionDisabledFn", publicName: "isOptionDisabledFn", isSignal: false, isRequired: false, transformFunction: null }, isOptionHiddenFn: { classPropertyName: "isOptionHiddenFn", publicName: "isOptionHiddenFn", isSignal: false, isRequired: false, transformFunction: null }, optionValueConverterFn: { classPropertyName: "optionValueConverterFn", publicName: "optionValueConverterFn", isSignal: false, isRequired: false, transformFunction: null }, activeEntity: { classPropertyName: "activeEntity", publicName: "activeEntity", isSignal: true, isRequired: false, transformFunction: null }, enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: false, isRequired: false, transformFunction: null }, valueTemplate: { classPropertyName: "valueTemplate", publicName: "valueTemplate", isSignal: false, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: false, isRequired: false, transformFunction: null }, displayPropertyResolver: { classPropertyName: "displayPropertyResolver", publicName: "displayPropertyResolver", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected" }, queries: [{ propertyName: "valueTemplateQuery", first: true, predicate: ElderSelectValueDirective, descendants: true, read: TemplateRef, static: true }], viewQueries: [{ propertyName: "matAutocomplete", first: true, predicate: ["auto"], descendants: true }], exportAs: ["elderSuggestionPanel"], ngImport: i0, template: "<mat-autocomplete\n #auto=\"matAutocomplete\"\n panelWidth=\"auto\"\n [autoActiveFirstOption]=\"true\"\n (opened)=\"onAutocompleteOpened($event)\"\n (optionSelected)=\"onOptionSelected($event)\"\n elderInfiniteScroll\n elderElderInfiniteAutocomplete\n (closeToEnd)=\"onAutoCompleteCloseToEnd()\"\n>\n @if (dataContext$ | async; as dc) {\n @if (dc.isClosed) {\n <mat-option disabled>\n <div class=\"layout-row place-start-center gap-sm\">\n <mat-icon color=\"warn\">warning</mat-icon>\n <span class=\"mat-caption\">DataContext Closed!</span>\n </div>\n </mat-option>\n }\n\n @if (availableSuggestions$ | async; as suggestions) {\n @if (suggestions.length === 0) {\n <mat-option disabled>No Data.</mat-option>\n }\n\n @for (suggestion of suggestions; track getIdAsString(suggestion)) {\n @if (isOptionVisible(suggestion)) {\n <mat-option\n [value]=\"toOptionValue(suggestion)\"\n [id]=\"getIdAsString(suggestion)\"\n [disabled]=\"!isOptionAvailable(suggestion)\"\n [class.active-option]=\"isOptionActive(suggestion)\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate || simpleValueTemplate;\n context: { $implicit: suggestion }\n \"\n >\n </ng-container>\n <!--\n <span class=\"mat-caption\">value: {{toOptionValue(suggestion)}}</span>\n -->\n </mat-option>\n }\n }\n }\n } @else {\n <mat-option disabled>\n <span class=\"mat-caption\">\n No DataContext!\n @if (dataSource$ | async; as ds) {\n (DataSource: {{ ds ? 'available' : 'missing' }})\n {{ enabled ? 'Autocomplete Enabled' : 'Autocomplete DISABLED' }}\n }\n </span>\n </mat-option>\n }\n\n @if (dataState$ | async; as state) {\n @if (!state.idle || state.loading) {\n <div style=\"position: relative\">\n <div style=\"position: absolute; right: 0; bottom: -8px; left: 0\">\n <mat-progress-bar\n [value]=\"100\"\n [mode]=\"state.loading ? 'query' : 'determinate'\"\n [color]=\"state.error ? 'warn' : 'primary'\"\n ></mat-progress-bar>\n </div>\n </div>\n }\n }\n</mat-autocomplete>\n\n<ng-template #simpleValueTemplate let-value>\n @if (displayPropertyResolver$ | async; as propertyResolver) {\n <span class=\"noselect\">{{ propertyResolver(value) }}</span>\n }\n</ng-template>\n", styles: [".active-option.active-option{font-weight:700}.active-option.active-option:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:4px;background-color:var(--md-sys-color-primary)}\n"], dependencies: [{ kind: "component", type: MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: ElderInfiniteAutocompleteDirective, selector: "mat-autocomplete[elderElderInfiniteAutocomplete]" }, { kind: "directive", type: ElderInfiniteScrollDirective, selector: "[elderInfiniteScroll]", inputs: ["listenToHost", "eventThrottle", "offsetFactor", "ignoreScrollEvent", "containerId", "scrollContainer"], outputs: ["closeToEnd", "scrolling"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24137
24145
  }
24138
24146
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ElderSuggestionPanelComponent, decorators: [{
24139
24147
  type: Component,
@@ -24146,7 +24154,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
24146
24154
  NgTemplateOutlet,
24147
24155
  MatProgressBar,
24148
24156
  AsyncPipe,
24149
- ], template: "<mat-autocomplete\n #auto=\"matAutocomplete\"\n panelWidth=\"auto\"\n [autoActiveFirstOption]=\"true\"\n (opened)=\"onAutocompleteOpened($event)\"\n (optionSelected)=\"onOptionSelected($event)\"\n elderInfiniteScroll\n elderElderInfiniteAutocomplete\n (closeToEnd)=\"onAutoCompleteCloseToEnd()\"\n>\n @if (dataContext$ | async; as dc) {\n @if (dc.isClosed) {\n <mat-option disabled>\n <div class=\"layout-row place-start-center gap-sm\">\n <mat-icon color=\"warn\">warning</mat-icon>\n <span class=\"mat-caption\">DataContext Closed!</span>\n </div>\n </mat-option>\n }\n\n @if (availableSuggestions$ | async; as suggestions) {\n @if (suggestions.length === 0) {\n <mat-option disabled>No Data.</mat-option>\n }\n\n @for (suggestion of suggestions; track getIdAsString(suggestion)) {\n @if (isOptionVisible(suggestion)) {\n <mat-option\n [value]=\"toOptionValue(suggestion)\"\n [id]=\"getIdAsString(suggestion)\"\n [disabled]=\"!isOptionAvailable(suggestion)\"\n [class.active-option]=\"isOptionActive(suggestion)\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate || simpleValueTemplate;\n context: { $implicit: suggestion }\n \"\n >\n </ng-container>\n <!--\n <span class=\"mat-caption\">value: {{toOptionValue(suggestion)}}</span>\n -->\n </mat-option>\n }\n }\n }\n } @else {\n <mat-option disabled>\n <span class=\"mat-caption\">\n No DataContext!\n @if (dataSource$ | async; as ds) {\n (DataSource: {{ ds ? 'available' : 'missing' }})\n {{ enabled ? 'Autocomplete Enabled' : 'Autocomplete DISABLED' }}\n }\n </span>\n </mat-option>\n }\n\n @if (dataState$ | async; as state) {\n @if (!state.idle || state.loading) {\n <div style=\"position: relative\">\n <div style=\"position: absolute; right: 0; bottom: -8px; left: 0\">\n <mat-progress-bar\n [value]=\"100\"\n [mode]=\"state.loading ? 'query' : 'determinate'\"\n [color]=\"state.error ? 'warn' : 'primary'\"\n ></mat-progress-bar>\n </div>\n </div>\n }\n }\n</mat-autocomplete>\n\n<ng-template #simpleValueTemplate let-value>\n @if (displayPropertyResolver$ | async; as propertyResolver) {\n <span class=\"noselect\">{{ propertyResolver(value) }}</span>\n }\n</ng-template>\n", styles: [".active-option.active-option{font-weight:700}.active-option.active-option:before{font-family:Material Icons;content:\"check\";font-size:13px;position:absolute;top:50%;left:1px;transform:translateY(-50%)}\n"] }]
24157
+ ], template: "<mat-autocomplete\n #auto=\"matAutocomplete\"\n panelWidth=\"auto\"\n [autoActiveFirstOption]=\"true\"\n (opened)=\"onAutocompleteOpened($event)\"\n (optionSelected)=\"onOptionSelected($event)\"\n elderInfiniteScroll\n elderElderInfiniteAutocomplete\n (closeToEnd)=\"onAutoCompleteCloseToEnd()\"\n>\n @if (dataContext$ | async; as dc) {\n @if (dc.isClosed) {\n <mat-option disabled>\n <div class=\"layout-row place-start-center gap-sm\">\n <mat-icon color=\"warn\">warning</mat-icon>\n <span class=\"mat-caption\">DataContext Closed!</span>\n </div>\n </mat-option>\n }\n\n @if (availableSuggestions$ | async; as suggestions) {\n @if (suggestions.length === 0) {\n <mat-option disabled>No Data.</mat-option>\n }\n\n @for (suggestion of suggestions; track getIdAsString(suggestion)) {\n @if (isOptionVisible(suggestion)) {\n <mat-option\n [value]=\"toOptionValue(suggestion)\"\n [id]=\"getIdAsString(suggestion)\"\n [disabled]=\"!isOptionAvailable(suggestion)\"\n [class.active-option]=\"isOptionActive(suggestion)\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n valueTemplate || simpleValueTemplate;\n context: { $implicit: suggestion }\n \"\n >\n </ng-container>\n <!--\n <span class=\"mat-caption\">value: {{toOptionValue(suggestion)}}</span>\n -->\n </mat-option>\n }\n }\n }\n } @else {\n <mat-option disabled>\n <span class=\"mat-caption\">\n No DataContext!\n @if (dataSource$ | async; as ds) {\n (DataSource: {{ ds ? 'available' : 'missing' }})\n {{ enabled ? 'Autocomplete Enabled' : 'Autocomplete DISABLED' }}\n }\n </span>\n </mat-option>\n }\n\n @if (dataState$ | async; as state) {\n @if (!state.idle || state.loading) {\n <div style=\"position: relative\">\n <div style=\"position: absolute; right: 0; bottom: -8px; left: 0\">\n <mat-progress-bar\n [value]=\"100\"\n [mode]=\"state.loading ? 'query' : 'determinate'\"\n [color]=\"state.error ? 'warn' : 'primary'\"\n ></mat-progress-bar>\n </div>\n </div>\n }\n }\n</mat-autocomplete>\n\n<ng-template #simpleValueTemplate let-value>\n @if (displayPropertyResolver$ | async; as propertyResolver) {\n <span class=\"noselect\">{{ propertyResolver(value) }}</span>\n }\n</ng-template>\n", styles: [".active-option.active-option{font-weight:700}.active-option.active-option:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:4px;background-color:var(--md-sys-color-primary)}\n"] }]
24150
24158
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.DestroyRef }], propDecorators: { valueTemplateQuery: [{
24151
24159
  type: ContentChild,
24152
24160
  args: [ElderSelectValueDirective, { read: TemplateRef, static: true }]
@@ -24409,7 +24417,9 @@ class ElderSelectComponent extends ElderSelectBase {
24409
24417
  if (this.isOptionDisabledFn) {
24410
24418
  return this.isOptionDisabledFn(option);
24411
24419
  }
24412
- return false;
24420
+ else {
24421
+ return false;
24422
+ }
24413
24423
  };
24414
24424
  }
24415
24425
  get isOptionHiddenInternalFn() {
@@ -24460,9 +24470,11 @@ class ElderSelectComponent extends ElderSelectBase {
24460
24470
  }
24461
24471
  const selectedEntity = optionSelected.entity;
24462
24472
  if (this.isEntitySelected(selectedEntity)) {
24463
- // Force signal emission when re-selecting the same entity.
24464
- // Signals skip emission when the value hasn't changed (by reference),
24465
- // so we reset to null and then restore the value to trigger subscribers.
24473
+ /*
24474
+ * Force signal emission when re-selecting the same entity.
24475
+ * Signals skip emission when the value hasn't changed (by reference),
24476
+ * therefore we need to null and then restore the value to trigger subscribers.
24477
+ */
24466
24478
  this.writeValueInternal(null);
24467
24479
  queueMicrotask(() => {
24468
24480
  this.updateValueByEntity(selectedEntity);
@@ -26408,7 +26420,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
26408
26420
  }], propDecorators: { stateColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateColor", required: false }] }], levelColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "levelColor", required: false }] }], namedColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "namedColor", required: false }] }], themeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], chipSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "chipSize", required: false }] }] } });
26409
26421
 
26410
26422
  class ChipModel {
26411
- constructor(id, value, displayText, colorSpec, removable, avatarSpec, trailingSpec) {
26423
+ constructor(id, value, displayText, colorSpec, removable, avatarSpec, trailingSpec, indicatorSpec) {
26412
26424
  this.id = id;
26413
26425
  this.value = value;
26414
26426
  this.displayText = displayText;
@@ -26416,6 +26428,7 @@ class ChipModel {
26416
26428
  this.removable = removable;
26417
26429
  this.avatarSpec = avatarSpec;
26418
26430
  this.trailingSpec = trailingSpec;
26431
+ this.indicatorSpec = indicatorSpec;
26419
26432
  }
26420
26433
  }
26421
26434
  class ElderMultiSelectChipsComponent extends ElderMultiSelectBase {
@@ -26590,7 +26603,7 @@ class ElderMultiSelectChipsComponent extends ElderMultiSelectBase {
26590
26603
  removable = true;
26591
26604
  }
26592
26605
  }
26593
- return new ChipModel(this.getEntityId(e), e, dPR(e), chipSpec?.colorSpec, removable, chipSpec.avatarSpec, chipSpec.trailingSpec);
26606
+ return new ChipModel(this.getEntityId(e), e, dPR(e), chipSpec?.colorSpec, removable, chipSpec.avatarSpec, chipSpec.trailingSpec, chipSpec.indicatorSpec);
26594
26607
  }
26595
26608
  reduceDisplayedChips(models, count) {
26596
26609
  const reducedChips = models.slice(0, count);
@@ -26611,7 +26624,7 @@ class ElderMultiSelectChipsComponent extends ElderMultiSelectBase {
26611
26624
  provide: ELDER_SELECT_BASE,
26612
26625
  useExisting: forwardRef(() => ElderMultiSelectChipsComponent),
26613
26626
  },
26614
- ], queries: [{ propertyName: "_customChipInput", first: true, predicate: MatFormFieldControl, descendants: true }, { propertyName: "chipTemplateQuery", first: true, predicate: ElderSelectChipDirective, descendants: true, read: TemplateRef }, { propertyName: "chipAvatarTemplateQuery", first: true, predicate: ElderSelectChipAvatarDirective, descendants: true, read: TemplateRef }, { propertyName: "customInputTemplateQuery", first: true, predicate: ElderSelectCustomInputDirective, descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "_chipInput", first: true, predicate: ElderSelectComponent, descendants: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"elder-flex-control\" [matTooltip]=\"state()?.error\">\n <mat-chip-set\n #chips\n [class.mat-mdc-chip-set-stacked]=\"stacked\"\n cdkDropList\n [cdkDropListOrientation]=\"stacked ? 'vertical' : 'horizontal'\"\n (cdkDropListDropped)=\"drop($event)\"\n [cdkDropListDisabled]=\"!allowSorting\"\n >\n @if (icon) {\n <div class=\"elder-input-prefix-icon-container flex-none\">\n <mat-icon\n disabled\n class=\"elder-prefix-icon elder-mdc-control-icon elder-icon-small noselect\"\n [class.loading]=\"state()?.loading\"\n >\n {{ icon }}\n </mat-icon>\n </div>\n }\n\n @for (chipModel of selectChips(); track chipModel.id) {\n <mat-chip-row\n elderChipLabel\n highlighted\n class=\"noselect clickable-chip\"\n [value]=\"resolveChipValue(chipModel.value)\"\n [color]=\"chipModel.colorSpec?.themeColor\"\n [levelColor]=\"chipModel.colorSpec?.levelColor\"\n [stateColor]=\"chipModel.colorSpec?.stateColor\"\n [namedColor]=\"chipModel.colorSpec?.namedColor\"\n [removable]=\"chipModel.removable\"\n (keydown)=\"onChipKeyDown($event, chipModel.value)\"\n (click)=\"onCurrentClicked(chipModel.value)\"\n cdkDrag\n [cdkDragData]=\"chipModel.value\"\n [cdkDragDisabled]=\"!allowSorting\"\n >\n @if (templates()?.avatar && !chipModel.avatarSpec?.hide) {\n <mat-chip-avatar [class.chip-avatar-xl]=\"chipModel.avatarSpec?.large\">\n <ng-container *ngTemplateOutlet=\"templates().avatar; context: { $implicit: chipModel }\">\n </ng-container>\n </mat-chip-avatar>\n }\n\n <ng-container\n *ngTemplateOutlet=\"\n templates()?.chip || simpleChipTemplate;\n context: { $implicit: chipModel }\n \"\n >\n </ng-container>\n\n @if (chipModel.trailingSpec?.icon; as trailingIcon) {\n <mat-icon\n matChipTrailingIcon\n class=\"elder-trailing-icon\"\n [fontSet]=\"chipModel.trailingSpec?.iconFontSet\"\n >{{ trailingIcon }}</mat-icon\n >\n }\n\n @if (chipModel.removable) {\n <mat-icon matChipRemove (click)=\"onClickRemoveChip($event, chipModel.value)\">\n cancel\n </mat-icon>\n }\n </mat-chip-row>\n }\n\n <div class=\"layout-row place-start-center elder-chip-input\">\n <!-- [matChipInputFor]=\"chips\" -->\n <ng-container *ngTemplateOutlet=\"templates()?.input || selectInput\"> </ng-container>\n\n @if (selectionPopup) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"elder-control-icon-button elder-browse-icon\"\n [disabled]=\"isLocked\"\n (click)=\"openSelectionPopup($event)\"\n aria-label=\"Search\"\n elderStopEventPropagation\n tabIndex=\"-1\"\n >\n <mat-icon class=\"elder-mdc-control-icon\">search</mat-icon>\n </button>\n }\n </div>\n </mat-chip-set>\n</div>\n\n<ng-template #selectInput>\n <!-- mat-mdc-chip-input -->\n <elder-select\n autocomplete\n elderClearSelect\n class=\"elder-chip-input-select flex\"\n [data]=\"dataContextS()\"\n [disabled]=\"!!disabledS()\"\n [required]=\"!!requiredS()\"\n [readonly]=\"!!readonlyS()\"\n [placeholder]=\"!readonlyS() ? placeholderS() : undefined\"\n (entityUpdated)=\"appendEntity($event)\"\n [displayPropertyResolver]=\"displayPropertyResolverS()\"\n [valueTemplate]=\"valueTemplate\"\n [queryFilter]=\"queryFilter\"\n [filters]=\"filters\"\n [sorts]=\"sorts\"\n [isOptionDisabledFn]=\"isOptionDisabledInternalFn\"\n [isOptionHiddenFn]=\"isOptionHiddenInternalFn\"\n ></elder-select>\n</ng-template>\n\n<ng-template #simpleChipTemplate let-chipModel>\n <span\n class=\"elder-chip-text\"\n [matTooltip]=\"chipModel.displayText\"\n [matTooltipDisabled]=\"chipModel.displayText?.length < 20\"\n >{{ chipModel.displayText | elderTruncate: 20 }}\n </span>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "directive", type: ElderChipLabelDirective, selector: "[elderChipLabel]", inputs: ["stateColor", "levelColor", "namedColor", "color", "chipSize"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: MatChipAvatar, selector: "mat-chip-avatar, [matChipAvatar]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: MatChipTrailingIcon, selector: "mat-chip-trailing-icon, [matChipTrailingIcon]" }, { kind: "directive", type: MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: ElderStopEventPropagationDirective, selector: "[elderStopEventPropagation]" }, { kind: "component", type: ElderSelectComponent, selector: "elder-select", inputs: ["nullDisplay", "autocomplete", "allowNull", "entity", "entityId"], outputs: ["entityIdChange", "entityIdUpdated", "entityChange", "entityUpdated", "entity"] }, { kind: "directive", type: ElderClearSelectDirective, selector: "[elderClearSelect]" }, { kind: "pipe", type: ElderTruncatePipe, name: "elderTruncate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
26627
+ ], queries: [{ propertyName: "_customChipInput", first: true, predicate: MatFormFieldControl, descendants: true }, { propertyName: "chipTemplateQuery", first: true, predicate: ElderSelectChipDirective, descendants: true, read: TemplateRef }, { propertyName: "chipAvatarTemplateQuery", first: true, predicate: ElderSelectChipAvatarDirective, descendants: true, read: TemplateRef }, { propertyName: "customInputTemplateQuery", first: true, predicate: ElderSelectCustomInputDirective, descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "_chipInput", first: true, predicate: ElderSelectComponent, descendants: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"elder-flex-control\" [matTooltip]=\"state()?.error\">\n <mat-chip-set\n #chips\n [class.mat-mdc-chip-set-stacked]=\"stacked\"\n cdkDropList\n [cdkDropListOrientation]=\"stacked ? 'vertical' : 'horizontal'\"\n (cdkDropListDropped)=\"drop($event)\"\n [cdkDropListDisabled]=\"!allowSorting\"\n >\n @if (icon) {\n <div class=\"elder-input-prefix-icon-container flex-none\">\n <mat-icon\n disabled\n class=\"elder-prefix-icon elder-mdc-control-icon elder-icon-small noselect\"\n [class.loading]=\"state()?.loading\"\n >\n {{ icon }}\n </mat-icon>\n </div>\n }\n\n @for (chipModel of selectChips(); track chipModel.id) {\n <mat-chip-row\n elderChipLabel\n highlighted\n class=\"noselect clickable-chip\"\n [value]=\"resolveChipValue(chipModel.value)\"\n [color]=\"chipModel.colorSpec?.themeColor\"\n [levelColor]=\"chipModel.colorSpec?.levelColor\"\n [stateColor]=\"chipModel.colorSpec?.stateColor\"\n [namedColor]=\"chipModel.colorSpec?.namedColor\"\n [removable]=\"chipModel.removable\"\n (keydown)=\"onChipKeyDown($event, chipModel.value)\"\n (click)=\"onCurrentClicked(chipModel.value)\"\n cdkDrag\n [cdkDragData]=\"chipModel.value\"\n [cdkDragDisabled]=\"!allowSorting\"\n [matBadge]=\"chipModel.indicatorSpec?.content\"\n [matBadgeColor]=\"chipModel.indicatorSpec?.themeColor || 'warn'\"\n [matBadgeSize]=\"chipModel.indicatorSpec?.size || 'small'\"\n [matBadgePosition]=\"chipModel.indicatorSpec?.position || 'above after'\"\n [matBadgeHidden]=\"!chipModel.indicatorSpec?.content\"\n >\n @if (templates()?.avatar && !chipModel.avatarSpec?.hide) {\n <mat-chip-avatar [class.chip-avatar-xl]=\"chipModel.avatarSpec?.large\">\n <ng-container *ngTemplateOutlet=\"templates().avatar; context: { $implicit: chipModel }\">\n </ng-container>\n </mat-chip-avatar>\n }\n\n <ng-container\n *ngTemplateOutlet=\"\n templates()?.chip || simpleChipTemplate;\n context: { $implicit: chipModel }\n \"\n >\n </ng-container>\n\n @if (chipModel.trailingSpec?.icon; as trailingIcon) {\n <mat-icon\n matChipTrailingIcon\n class=\"elder-trailing-icon\"\n [fontSet]=\"chipModel.trailingSpec?.iconFontSet\"\n >{{ trailingIcon }}</mat-icon\n >\n }\n\n @if (chipModel.removable) {\n <mat-icon matChipRemove (click)=\"onClickRemoveChip($event, chipModel.value)\">\n cancel\n </mat-icon>\n }\n </mat-chip-row>\n }\n\n <div class=\"layout-row place-start-center elder-chip-input\">\n <!-- [matChipInputFor]=\"chips\" -->\n <ng-container *ngTemplateOutlet=\"templates()?.input || selectInput\"> </ng-container>\n\n @if (selectionPopup) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"elder-control-icon-button elder-browse-icon\"\n [disabled]=\"isLocked\"\n (click)=\"openSelectionPopup($event)\"\n aria-label=\"Search\"\n elderStopEventPropagation\n tabIndex=\"-1\"\n >\n <mat-icon class=\"elder-mdc-control-icon\">search</mat-icon>\n </button>\n }\n </div>\n </mat-chip-set>\n</div>\n\n<ng-template #selectInput>\n <!-- mat-mdc-chip-input -->\n <elder-select\n autocomplete\n elderClearSelect\n class=\"elder-chip-input-select flex\"\n [data]=\"dataContextS()\"\n [disabled]=\"!!disabledS()\"\n [required]=\"!!requiredS()\"\n [readonly]=\"!!readonlyS()\"\n [placeholder]=\"!readonlyS() ? placeholderS() : undefined\"\n (entityUpdated)=\"appendEntity($event)\"\n [displayPropertyResolver]=\"displayPropertyResolverS()\"\n [valueTemplate]=\"valueTemplate\"\n [queryFilter]=\"queryFilter\"\n [filters]=\"filters\"\n [sorts]=\"sorts\"\n [isOptionDisabledFn]=\"isOptionDisabledInternalFn\"\n [isOptionHiddenFn]=\"isOptionHiddenInternalFn\"\n ></elder-select>\n</ng-template>\n\n<ng-template #simpleChipTemplate let-chipModel>\n <span\n class=\"elder-chip-text\"\n [matTooltip]=\"chipModel.displayText\"\n [matTooltipDisabled]=\"chipModel.displayText?.length < 20\"\n >{{ chipModel.displayText | elderTruncate: 20 }}\n </span>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "directive", type: ElderChipLabelDirective, selector: "[elderChipLabel]", inputs: ["stateColor", "levelColor", "namedColor", "color", "chipSize"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: MatChipAvatar, selector: "mat-chip-avatar, [matChipAvatar]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: MatChipTrailingIcon, selector: "mat-chip-trailing-icon, [matChipTrailingIcon]" }, { kind: "directive", type: MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: ElderStopEventPropagationDirective, selector: "[elderStopEventPropagation]" }, { kind: "component", type: ElderSelectComponent, selector: "elder-select", inputs: ["nullDisplay", "autocomplete", "allowNull", "entity", "entityId"], outputs: ["entityIdChange", "entityIdUpdated", "entityChange", "entityUpdated", "entity"] }, { kind: "directive", type: ElderClearSelectDirective, selector: "[elderClearSelect]" }, { kind: "directive", type: MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "pipe", type: ElderTruncatePipe, name: "elderTruncate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
26615
26628
  }
26616
26629
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ElderMultiSelectChipsComponent, decorators: [{
26617
26630
  type: Component,
@@ -26638,7 +26651,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
26638
26651
  ElderSelectComponent,
26639
26652
  ElderClearSelectDirective,
26640
26653
  ElderTruncatePipe,
26641
- ], template: "<div class=\"elder-flex-control\" [matTooltip]=\"state()?.error\">\n <mat-chip-set\n #chips\n [class.mat-mdc-chip-set-stacked]=\"stacked\"\n cdkDropList\n [cdkDropListOrientation]=\"stacked ? 'vertical' : 'horizontal'\"\n (cdkDropListDropped)=\"drop($event)\"\n [cdkDropListDisabled]=\"!allowSorting\"\n >\n @if (icon) {\n <div class=\"elder-input-prefix-icon-container flex-none\">\n <mat-icon\n disabled\n class=\"elder-prefix-icon elder-mdc-control-icon elder-icon-small noselect\"\n [class.loading]=\"state()?.loading\"\n >\n {{ icon }}\n </mat-icon>\n </div>\n }\n\n @for (chipModel of selectChips(); track chipModel.id) {\n <mat-chip-row\n elderChipLabel\n highlighted\n class=\"noselect clickable-chip\"\n [value]=\"resolveChipValue(chipModel.value)\"\n [color]=\"chipModel.colorSpec?.themeColor\"\n [levelColor]=\"chipModel.colorSpec?.levelColor\"\n [stateColor]=\"chipModel.colorSpec?.stateColor\"\n [namedColor]=\"chipModel.colorSpec?.namedColor\"\n [removable]=\"chipModel.removable\"\n (keydown)=\"onChipKeyDown($event, chipModel.value)\"\n (click)=\"onCurrentClicked(chipModel.value)\"\n cdkDrag\n [cdkDragData]=\"chipModel.value\"\n [cdkDragDisabled]=\"!allowSorting\"\n >\n @if (templates()?.avatar && !chipModel.avatarSpec?.hide) {\n <mat-chip-avatar [class.chip-avatar-xl]=\"chipModel.avatarSpec?.large\">\n <ng-container *ngTemplateOutlet=\"templates().avatar; context: { $implicit: chipModel }\">\n </ng-container>\n </mat-chip-avatar>\n }\n\n <ng-container\n *ngTemplateOutlet=\"\n templates()?.chip || simpleChipTemplate;\n context: { $implicit: chipModel }\n \"\n >\n </ng-container>\n\n @if (chipModel.trailingSpec?.icon; as trailingIcon) {\n <mat-icon\n matChipTrailingIcon\n class=\"elder-trailing-icon\"\n [fontSet]=\"chipModel.trailingSpec?.iconFontSet\"\n >{{ trailingIcon }}</mat-icon\n >\n }\n\n @if (chipModel.removable) {\n <mat-icon matChipRemove (click)=\"onClickRemoveChip($event, chipModel.value)\">\n cancel\n </mat-icon>\n }\n </mat-chip-row>\n }\n\n <div class=\"layout-row place-start-center elder-chip-input\">\n <!-- [matChipInputFor]=\"chips\" -->\n <ng-container *ngTemplateOutlet=\"templates()?.input || selectInput\"> </ng-container>\n\n @if (selectionPopup) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"elder-control-icon-button elder-browse-icon\"\n [disabled]=\"isLocked\"\n (click)=\"openSelectionPopup($event)\"\n aria-label=\"Search\"\n elderStopEventPropagation\n tabIndex=\"-1\"\n >\n <mat-icon class=\"elder-mdc-control-icon\">search</mat-icon>\n </button>\n }\n </div>\n </mat-chip-set>\n</div>\n\n<ng-template #selectInput>\n <!-- mat-mdc-chip-input -->\n <elder-select\n autocomplete\n elderClearSelect\n class=\"elder-chip-input-select flex\"\n [data]=\"dataContextS()\"\n [disabled]=\"!!disabledS()\"\n [required]=\"!!requiredS()\"\n [readonly]=\"!!readonlyS()\"\n [placeholder]=\"!readonlyS() ? placeholderS() : undefined\"\n (entityUpdated)=\"appendEntity($event)\"\n [displayPropertyResolver]=\"displayPropertyResolverS()\"\n [valueTemplate]=\"valueTemplate\"\n [queryFilter]=\"queryFilter\"\n [filters]=\"filters\"\n [sorts]=\"sorts\"\n [isOptionDisabledFn]=\"isOptionDisabledInternalFn\"\n [isOptionHiddenFn]=\"isOptionHiddenInternalFn\"\n ></elder-select>\n</ng-template>\n\n<ng-template #simpleChipTemplate let-chipModel>\n <span\n class=\"elder-chip-text\"\n [matTooltip]=\"chipModel.displayText\"\n [matTooltipDisabled]=\"chipModel.displayText?.length < 20\"\n >{{ chipModel.displayText | elderTruncate: 20 }}\n </span>\n</ng-template>\n" }]
26654
+ MatBadge,
26655
+ ], template: "<div class=\"elder-flex-control\" [matTooltip]=\"state()?.error\">\n <mat-chip-set\n #chips\n [class.mat-mdc-chip-set-stacked]=\"stacked\"\n cdkDropList\n [cdkDropListOrientation]=\"stacked ? 'vertical' : 'horizontal'\"\n (cdkDropListDropped)=\"drop($event)\"\n [cdkDropListDisabled]=\"!allowSorting\"\n >\n @if (icon) {\n <div class=\"elder-input-prefix-icon-container flex-none\">\n <mat-icon\n disabled\n class=\"elder-prefix-icon elder-mdc-control-icon elder-icon-small noselect\"\n [class.loading]=\"state()?.loading\"\n >\n {{ icon }}\n </mat-icon>\n </div>\n }\n\n @for (chipModel of selectChips(); track chipModel.id) {\n <mat-chip-row\n elderChipLabel\n highlighted\n class=\"noselect clickable-chip\"\n [value]=\"resolveChipValue(chipModel.value)\"\n [color]=\"chipModel.colorSpec?.themeColor\"\n [levelColor]=\"chipModel.colorSpec?.levelColor\"\n [stateColor]=\"chipModel.colorSpec?.stateColor\"\n [namedColor]=\"chipModel.colorSpec?.namedColor\"\n [removable]=\"chipModel.removable\"\n (keydown)=\"onChipKeyDown($event, chipModel.value)\"\n (click)=\"onCurrentClicked(chipModel.value)\"\n cdkDrag\n [cdkDragData]=\"chipModel.value\"\n [cdkDragDisabled]=\"!allowSorting\"\n [matBadge]=\"chipModel.indicatorSpec?.content\"\n [matBadgeColor]=\"chipModel.indicatorSpec?.themeColor || 'warn'\"\n [matBadgeSize]=\"chipModel.indicatorSpec?.size || 'small'\"\n [matBadgePosition]=\"chipModel.indicatorSpec?.position || 'above after'\"\n [matBadgeHidden]=\"!chipModel.indicatorSpec?.content\"\n >\n @if (templates()?.avatar && !chipModel.avatarSpec?.hide) {\n <mat-chip-avatar [class.chip-avatar-xl]=\"chipModel.avatarSpec?.large\">\n <ng-container *ngTemplateOutlet=\"templates().avatar; context: { $implicit: chipModel }\">\n </ng-container>\n </mat-chip-avatar>\n }\n\n <ng-container\n *ngTemplateOutlet=\"\n templates()?.chip || simpleChipTemplate;\n context: { $implicit: chipModel }\n \"\n >\n </ng-container>\n\n @if (chipModel.trailingSpec?.icon; as trailingIcon) {\n <mat-icon\n matChipTrailingIcon\n class=\"elder-trailing-icon\"\n [fontSet]=\"chipModel.trailingSpec?.iconFontSet\"\n >{{ trailingIcon }}</mat-icon\n >\n }\n\n @if (chipModel.removable) {\n <mat-icon matChipRemove (click)=\"onClickRemoveChip($event, chipModel.value)\">\n cancel\n </mat-icon>\n }\n </mat-chip-row>\n }\n\n <div class=\"layout-row place-start-center elder-chip-input\">\n <!-- [matChipInputFor]=\"chips\" -->\n <ng-container *ngTemplateOutlet=\"templates()?.input || selectInput\"> </ng-container>\n\n @if (selectionPopup) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"elder-control-icon-button elder-browse-icon\"\n [disabled]=\"isLocked\"\n (click)=\"openSelectionPopup($event)\"\n aria-label=\"Search\"\n elderStopEventPropagation\n tabIndex=\"-1\"\n >\n <mat-icon class=\"elder-mdc-control-icon\">search</mat-icon>\n </button>\n }\n </div>\n </mat-chip-set>\n</div>\n\n<ng-template #selectInput>\n <!-- mat-mdc-chip-input -->\n <elder-select\n autocomplete\n elderClearSelect\n class=\"elder-chip-input-select flex\"\n [data]=\"dataContextS()\"\n [disabled]=\"!!disabledS()\"\n [required]=\"!!requiredS()\"\n [readonly]=\"!!readonlyS()\"\n [placeholder]=\"!readonlyS() ? placeholderS() : undefined\"\n (entityUpdated)=\"appendEntity($event)\"\n [displayPropertyResolver]=\"displayPropertyResolverS()\"\n [valueTemplate]=\"valueTemplate\"\n [queryFilter]=\"queryFilter\"\n [filters]=\"filters\"\n [sorts]=\"sorts\"\n [isOptionDisabledFn]=\"isOptionDisabledInternalFn\"\n [isOptionHiddenFn]=\"isOptionHiddenInternalFn\"\n ></elder-select>\n</ng-template>\n\n<ng-template #simpleChipTemplate let-chipModel>\n <span\n class=\"elder-chip-text\"\n [matTooltip]=\"chipModel.displayText\"\n [matTooltipDisabled]=\"chipModel.displayText?.length < 20\"\n >{{ chipModel.displayText | elderTruncate: 20 }}\n </span>\n</ng-template>\n" }]
26642
26656
  }], ctorParameters: () => [], propDecorators: { defaultChipSpec: [{
26643
26657
  type: Input
26644
26658
  }], chipSpecFn: [{