x4js 2.0.11 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +15 -15
  2. package/lib/README.txt +15 -15
  3. package/lib/cjs/x4.css +1 -1
  4. package/lib/cjs/x4.js +1 -1
  5. package/lib/esm/x4.css +1 -1
  6. package/lib/esm/x4.mjs +1 -1
  7. package/lib/src/components/base.scss +25 -26
  8. package/lib/src/components/boxes/boxes.module.scss +37 -37
  9. package/lib/src/components/boxes/boxes.ts +129 -125
  10. package/lib/src/components/breadcrumb/breadcrumb.scss +28 -0
  11. package/lib/src/components/breadcrumb/breadcrumb.ts +84 -0
  12. package/lib/src/components/breadcrumb/chevron-right.svg +1 -0
  13. package/lib/src/components/btngroup/btngroup.module.scss +28 -28
  14. package/lib/src/components/btngroup/btngroup.ts +119 -101
  15. package/lib/src/components/button/button.module.scss +154 -153
  16. package/lib/src/components/button/button.ts +117 -117
  17. package/lib/src/components/calendar/calendar.module.scss +162 -162
  18. package/lib/src/components/calendar/calendar.ts +326 -325
  19. package/lib/src/components/checkbox/check.svg +3 -3
  20. package/lib/src/components/checkbox/checkbox.module.scss +141 -141
  21. package/lib/src/components/checkbox/checkbox.ts +125 -124
  22. package/lib/src/components/colorinput/colorinput.module.scss +64 -64
  23. package/lib/src/components/colorinput/colorinput.ts +90 -87
  24. package/lib/src/components/colorpicker/colorpicker.module.scss +132 -132
  25. package/lib/src/components/colorpicker/colorpicker.ts +481 -476
  26. package/lib/src/components/combobox/combobox.module.scss +123 -120
  27. package/lib/src/components/combobox/combobox.ts +192 -190
  28. package/lib/src/components/combobox/updown.svg +3 -3
  29. package/lib/src/components/components.ts +34 -0
  30. package/lib/src/components/dialog/dialog.module.scss +71 -71
  31. package/lib/src/components/dialog/dialog.ts +94 -92
  32. package/lib/src/components/form/form.module.scss +34 -34
  33. package/lib/src/components/form/form.ts +41 -36
  34. package/lib/src/components/grid/datastore.ts +1298 -0
  35. package/lib/src/components/grid/gridview.ts +1108 -0
  36. package/lib/src/components/grid/memdb.ts +325 -0
  37. package/lib/src/components/header/header.module.scss +39 -39
  38. package/lib/src/components/header/header.ts +129 -123
  39. package/lib/src/components/icon/icon.module.scss +29 -29
  40. package/lib/src/components/icon/icon.ts +136 -134
  41. package/lib/src/components/image/image.module.scss +20 -20
  42. package/lib/src/components/image/image.ts +68 -66
  43. package/lib/src/components/input/input.module.scss +69 -69
  44. package/lib/src/components/input/input.ts +275 -274
  45. package/lib/src/components/label/label.module.scss +58 -52
  46. package/lib/src/components/label/label.ts +64 -55
  47. package/lib/src/components/link/link.ts +78 -0
  48. package/lib/src/components/listbox/listbox.module.scss +103 -103
  49. package/lib/src/components/listbox/listbox.ts +431 -427
  50. package/lib/src/components/menu/menu.module.scss +107 -107
  51. package/lib/src/components/menu/menu.ts +171 -168
  52. package/lib/src/components/messages/messages.module.scss +48 -47
  53. package/lib/src/components/messages/messages.ts +68 -63
  54. package/lib/src/components/normalize.scss +386 -386
  55. package/lib/src/components/notification/notification.module.scss +81 -81
  56. package/lib/src/components/notification/notification.ts +109 -108
  57. package/lib/src/components/panel/panel.module.scss +47 -47
  58. package/lib/src/components/panel/panel.ts +57 -56
  59. package/lib/src/components/popup/popup.module.scss +43 -43
  60. package/lib/src/components/popup/popup.ts +396 -395
  61. package/lib/src/components/progress/progress.module.scss +56 -56
  62. package/lib/src/components/progress/progress.ts +43 -42
  63. package/lib/src/components/rating/rating.module.scss +22 -22
  64. package/lib/src/components/rating/rating.ts +131 -125
  65. package/lib/src/components/shared.scss +90 -76
  66. package/lib/src/components/sizers/sizer.module.scss +89 -89
  67. package/lib/src/components/sizers/sizer.ts +123 -119
  68. package/lib/src/components/slider/slider.module.scss +70 -70
  69. package/lib/src/components/slider/slider.ts +147 -142
  70. package/lib/src/components/switch/switch.module.scss +126 -126
  71. package/lib/src/components/switch/switch.ts +61 -55
  72. package/lib/src/components/tabs/tabs.module.scss +46 -46
  73. package/lib/src/components/tabs/tabs.ts +168 -157
  74. package/lib/src/components/textarea/textarea.module.scss +59 -59
  75. package/lib/src/components/textarea/textarea.ts +60 -54
  76. package/lib/src/components/textedit/textedit.module.scss +113 -113
  77. package/lib/src/components/textedit/textedit.ts +83 -82
  78. package/lib/src/components/themes.scss +81 -77
  79. package/lib/src/components/tooltips/tooltips.scss +50 -50
  80. package/lib/src/components/tooltips/tooltips.ts +103 -102
  81. package/lib/src/components/treeview/treeview.module.scss +115 -115
  82. package/lib/src/components/treeview/treeview.ts +410 -403
  83. package/lib/src/components/viewport/viewport.module.scss +24 -24
  84. package/lib/src/components/viewport/viewport.ts +41 -38
  85. package/lib/src/core/component.ts +1002 -979
  86. package/lib/src/core/core_application.ts +44 -0
  87. package/lib/src/core/core_colors.ts +249 -249
  88. package/lib/src/core/core_dom.ts +471 -471
  89. package/lib/src/core/core_dragdrop.ts +200 -200
  90. package/lib/src/core/core_element.ts +97 -97
  91. package/lib/src/core/core_events.ts +149 -149
  92. package/lib/src/core/core_i18n.ts +377 -377
  93. package/lib/src/core/core_router.ts +221 -221
  94. package/lib/src/core/core_styles.ts +214 -214
  95. package/lib/src/core/core_svg.ts +550 -550
  96. package/lib/src/core/core_tools.ts +688 -673
  97. package/lib/src/demo/assets/radio.svg +3 -3
  98. package/lib/src/demo/index.html +11 -11
  99. package/lib/src/demo/main.scss +21 -21
  100. package/lib/src/demo/main.tsx +323 -323
  101. package/lib/src/types/scss.d.ts +4 -4
  102. package/lib/src/types/x4react.d.ts +8 -8
  103. package/lib/src/x4.scss +18 -18
  104. package/lib/src/x4.ts +31 -60
  105. package/lib/styles/x4.css +1 -1
  106. package/lib/types/x4js.d.ts +100 -49
  107. package/package.json +2 -3
  108. package/src/x4.ts +31 -60
  109. package/lib/output.d.ts +0 -1472
@@ -0,0 +1,1298 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file datastore.ts
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2024 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ import { ComponentEvents, ComponentProps, EvChange } from '@core/component.js';
18
+ import { CoreElement } from '@core/core_element.js';
19
+ import { CoreEvent, EventCallback, EventMap, EventSource } from '@core/core_events.js';
20
+ import { isArray, isString } from '@core/core_tools.js';
21
+ import { Component } from 'x4js';
22
+
23
+
24
+
25
+ export type ChangeCallback = (type: string, id?: any) => void;
26
+ export type CalcCallback = () => string;
27
+
28
+ export type FieldType = 'string' | 'int' | 'float' | 'date' | 'bool' | 'array' | 'object' | 'any' | 'calc';
29
+ export type DataIndex = Uint32Array;
30
+
31
+ export interface EvDataChange extends CoreEvent {
32
+ change_type: 'create' | 'update' | 'delete' | 'data' | 'change';
33
+ id?: any;
34
+ }
35
+
36
+
37
+
38
+
39
+
40
+
41
+ /**
42
+ * fields definition
43
+ * field with index=0 is record id
44
+ */
45
+
46
+ export interface MetaData {
47
+ type?: FieldType;
48
+ prec?: number;
49
+ required?: boolean;
50
+ calc?: (rec: Record) => any;
51
+ model?: Record; // in case of array of subtypes, the model
52
+ }
53
+
54
+ export interface FieldInfo extends MetaData {
55
+ name: string;
56
+ }
57
+
58
+ /**
59
+ *
60
+ */
61
+
62
+ class MetaInfos {
63
+ name: string;
64
+ id: string; // field name holding 'id' record info
65
+ fields: FieldInfo[]; // field list
66
+
67
+ constructor( name: string ) {
68
+ this.name = name;
69
+ this.id = undefined;
70
+ this.fields = [];
71
+ }
72
+ }
73
+
74
+ const metaFields = Symbol( 'metaField' );
75
+
76
+ function _getMetas( obj: object, create = true ) : MetaInfos {
77
+
78
+ let ctor = obj.constructor as any;
79
+ let mfld = ctor.hasOwnProperty(metaFields) ? ctor[metaFields] : undefined;
80
+
81
+ if( mfld===undefined ) {
82
+ if( !create ) {
83
+ console.assert( mfld!==undefined );
84
+ }
85
+
86
+ // construct our metas
87
+ mfld = new MetaInfos( ctor.name );
88
+
89
+ // merge with parent class metas
90
+ let pctor = Object.getPrototypeOf(ctor);
91
+ if( pctor!=Record ) {
92
+ let pmetas = pctor[metaFields];
93
+ mfld.fields = [...pmetas.fields, ...mfld.fields ]
94
+
95
+ console.assert( mfld.id===undefined, 'cannot define mutiple record id' );
96
+ if( !mfld.id ) {
97
+ mfld.id = pmetas.id;
98
+ }
99
+ }
100
+
101
+ (obj.constructor as any)[metaFields] = mfld;
102
+ }
103
+
104
+ return mfld;
105
+ }
106
+
107
+ export namespace data {
108
+
109
+ /**
110
+ * define a record id
111
+ * @example
112
+ * \@data_id()
113
+ * id: string; // this field is the record id
114
+ **/
115
+
116
+ export function id( ) {
117
+ return ( ownerCls: any, fldName: string ) => {
118
+ let metas = _getMetas( ownerCls );
119
+ metas.fields.push( {
120
+ name: fldName,
121
+ type: 'any',
122
+ required: true,
123
+ });
124
+
125
+ metas.id = fldName;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * @ignore
131
+ */
132
+
133
+ export function field( data: MetaData ) {
134
+
135
+ return ( ownerCls: any, fldName: string ) => {
136
+ let metas = _getMetas( ownerCls );
137
+ metas.fields.push( {
138
+ name: fldName,
139
+ ...data
140
+ } );
141
+ }
142
+ }
143
+
144
+ /**
145
+ * following member is a string field
146
+ * @example
147
+ * \@data_string()
148
+ * my_field: string; // this field will be seen as a string
149
+ */
150
+
151
+ export function string( props?: MetaData ) {
152
+ return field( { ...props, type: 'string' } );
153
+ }
154
+
155
+ /**
156
+ * following member is an integer field
157
+ * @example
158
+ * \@data_string()
159
+ * my_field: number; // this field will be seen as an integer
160
+ */
161
+
162
+ export function int( props?: MetaData ) {
163
+ return field( { ...props, type: 'int' } );
164
+ }
165
+
166
+ /**
167
+ * following member is a float field
168
+ * @example
169
+ * \@data_float()
170
+ * my_field: number; // this field will be seen as a float
171
+ */
172
+
173
+ export function float( props?: MetaData ) {
174
+ return field( { ...props, type: 'float' } );
175
+ }
176
+
177
+ /**
178
+ * following member is a boolean field
179
+ * @example
180
+ * \@data_bool()
181
+ * my_field: boolean; // this field will be seen as a boolean
182
+ */
183
+
184
+ export function bool( props?: MetaData ) {
185
+ return field( { ...props, type: 'bool' } );
186
+ }
187
+
188
+ /**
189
+ * following member is a date field
190
+ * @example
191
+ * \@data_date()
192
+ * my_field: date; // this field will be seen as a date
193
+ */
194
+
195
+ export function date( props?: MetaData ) {
196
+ return field( { ...props, type: 'date' } );
197
+ }
198
+
199
+ /**
200
+ * following member is a calculated field
201
+ * @example
202
+ * \@data_calc( )
203
+ * get my_field(): string => {
204
+ * return 'hello';
205
+ * };
206
+ */
207
+
208
+ export function calc( props?: MetaData ) {
209
+ return field( { ...props, type: 'calc'} )
210
+ }
211
+
212
+
213
+
214
+ /**
215
+ *
216
+ */
217
+
218
+ interface RecordConstructor {
219
+ new ( data?: any, id?: any ): Record;
220
+ }
221
+
222
+ /**
223
+ * following member is a record array
224
+ * @example
225
+ * \@data_array( )
226
+ * my_field(): TypedRecord[];
227
+ */
228
+
229
+ export function array( ctor: RecordConstructor, props?: MetaData ) {
230
+ return data.field( { ...props, type: 'array', model: ctor ? new ctor() : null } )
231
+ }
232
+
233
+ }
234
+
235
+
236
+
237
+
238
+ /**
239
+ * record model
240
+ */
241
+
242
+ export class Record {
243
+ [ key: string ]: any;
244
+
245
+ constructor( data?: any, id?: any ) {
246
+
247
+ if( data!==undefined ) {
248
+ this.unSerialize( data, id );
249
+ }
250
+ }
251
+
252
+ clone( source?: any ) {
253
+ let rec = new (this.constructor as any)( );
254
+ if( source ) {
255
+ rec.unSerialize( source );
256
+ }
257
+ return rec;
258
+ }
259
+
260
+ /**
261
+ * get the record unique identifier
262
+ * by default the return value is the first field
263
+ * @return unique identifier
264
+ */
265
+
266
+ getID(): any {
267
+ let metas = _getMetas( this, false );
268
+ return this[metas.id];
269
+ }
270
+
271
+ /**
272
+ * MUST IMPLEMENT
273
+ * @returns fields descriptors
274
+ */
275
+
276
+ getFields(): FieldInfo[] {
277
+ let metas = _getMetas( this, false );
278
+ return metas.fields;
279
+ }
280
+
281
+ /**
282
+ *
283
+ */
284
+
285
+ validate( ) : Error[] {
286
+
287
+ let errs: Error[] = null;
288
+
289
+ let fields = this.getFields( );
290
+ fields.forEach( (fi) => {
291
+ if( fi.required && !this.getField(fi.name) ) {
292
+ if( errs ) {
293
+ errs = [];
294
+ }
295
+
296
+ errs.push( new Error( `field ${fi.name} is required.` ) );
297
+ }
298
+ })
299
+
300
+ return errs;
301
+ }
302
+
303
+ //mapAnyFields() {
304
+ // this.getFields = ( ) => {
305
+ // return Object.keys( this ).map( (name) => {
306
+ // return <FieldInfo>{ name };
307
+ // });
308
+ // }
309
+ //}
310
+
311
+ getFieldIndex( name: string ) : number {
312
+ let fields = this.getFields( );
313
+ return fields.findIndex( (fd) => fd.name == name );
314
+ }
315
+
316
+ /**
317
+ * default serializer
318
+ * @returns an object with known record values
319
+ */
320
+
321
+ serialize(): any {
322
+ let rec: any = {};
323
+
324
+ this.getFields().forEach((f) => {
325
+ if( f.calc === undefined ) {
326
+ rec[f.name] = rec[f.name];
327
+ }
328
+ });
329
+
330
+ return rec;
331
+ }
332
+
333
+ /**
334
+ * default unserializer
335
+ * @param data - input data
336
+ * @returns a new Record
337
+ */
338
+
339
+ unSerialize(data: any, id?: any) : Record {
340
+
341
+ let fields = this.getFields();
342
+
343
+ fields.forEach( (sf) => {
344
+ let value = data[sf.name];
345
+ if (value !== undefined) {
346
+ this[sf.name] = this._convertField( sf, value );
347
+ }
348
+ });
349
+
350
+ if( id!==undefined ) {
351
+ this[fields[0].name] = id;
352
+ }
353
+ else {
354
+ console.assert( this.getID()!==undefined ); // store do not have ID field
355
+ }
356
+
357
+ return this;
358
+ }
359
+
360
+ /**
361
+ * field conversion
362
+ * @param field - field descriptor
363
+ * @param input - value to convert
364
+ * @returns the field value in it's original form
365
+ */
366
+
367
+ protected _convertField( field: FieldInfo, input: any ) : any {
368
+
369
+ //TODO: boolean
370
+
371
+ switch( field.type ) {
372
+ case 'float': {
373
+ let ffv: number = typeof (input) === 'number' ? input : parseFloat(input);
374
+
375
+ if (field.prec !== undefined) {
376
+ let mul = Math.pow(10, field.prec);
377
+ ffv = Math.round(ffv * mul) / mul;
378
+ }
379
+
380
+ return ffv;
381
+ }
382
+
383
+ case 'int': {
384
+ return typeof (input) === 'number' ? input : parseInt(input);
385
+ }
386
+
387
+ case 'date': {
388
+ return isString(input) ? new Date(input) : input;
389
+ }
390
+
391
+ case 'array': {
392
+ let result: any[] = [];
393
+
394
+ if( field.model ) {
395
+ input.forEach( ( v: any ) => {
396
+ result.push( field.model.clone( v ) );
397
+ })
398
+
399
+ return result;
400
+ }
401
+ break;
402
+ }
403
+ }
404
+
405
+ return input;
406
+ }
407
+
408
+ /**
409
+ * get raw value of a field
410
+ * @param name - field name or field index
411
+ */
412
+
413
+ getRaw( name: string | number ) : any {
414
+
415
+ let idx;
416
+ let fields = this.getFields( );
417
+
418
+ if( typeof(name) === 'string' ) {
419
+ idx = fields.findIndex( fi => fi.name == name );
420
+ if( idx < 0 ) {
421
+ console.assert( false, 'unknown field: '+name);
422
+ return undefined;
423
+ }
424
+ }
425
+ else if( name<fields.length ) {
426
+ if( name<0 ) {
427
+ return undefined
428
+ }
429
+
430
+ idx = name;
431
+ }
432
+ else {
433
+ console.assert( false, 'bad field name: '+name);
434
+ return undefined;
435
+ }
436
+
437
+ let fld = fields[idx];
438
+ if( fld.calc!==undefined ) {
439
+ return fld.calc( this );
440
+ }
441
+
442
+ return this[fld.name];
443
+ }
444
+
445
+ /**
446
+ *
447
+ * @param name
448
+ * @param data
449
+ */
450
+
451
+ setRaw( name: string, data: string ) {
452
+ this[name] = data;
453
+ }
454
+
455
+ /**
456
+ * get field value (as string)
457
+ * @param name - field name
458
+ * @example
459
+ * let value = record.get('field1');
460
+ */
461
+
462
+ getField( name: string ): string {
463
+ let v = this.getRaw( name );
464
+ return (v===undefined || v===null) ? '' : ''+v;
465
+ }
466
+
467
+ /**
468
+ * set field value
469
+ * @param name - field name
470
+ * @param value - value to set
471
+ * @example
472
+ * record.set( 'field1', 7 );
473
+ */
474
+
475
+ setField(name: string, value: any) {
476
+ let fields = this.getFields( );
477
+ let idx = fields.findIndex( fi => fi.name == name );
478
+
479
+ if( idx < 0 ) {
480
+ console.assert( false, 'unknown field: '+name);
481
+ return;
482
+ }
483
+
484
+ let fld = fields[idx];
485
+ if( fld.calc!==undefined ) {
486
+ console.assert( false, 'cannot set calc field: '+name);
487
+ return;
488
+ }
489
+
490
+ this.setRaw( fld.name, value );
491
+ }
492
+ }
493
+
494
+ /**
495
+ * by default, the field id is rhe first member or the record
496
+ */
497
+
498
+ export class AutoRecord extends Record {
499
+
500
+ private m_data;
501
+ private m_fid: string;
502
+
503
+ constructor( data: any ) {
504
+ super( );
505
+
506
+ this.m_data = data;
507
+ }
508
+
509
+ getID( ) {
510
+ if( !this.m_fid ) {
511
+ let fnames = Object.keys( this.m_data );
512
+ this.m_fid = fnames[0];
513
+ }
514
+
515
+ return this.m_data[this.m_fid];
516
+ }
517
+
518
+ getFields( ) : FieldInfo[] {
519
+ let fnames = Object.keys( this.m_data );
520
+ let fields: FieldInfo[] = fnames.map( (n) => {
521
+ return {
522
+ name: n
523
+ };
524
+ })
525
+
526
+ return fields;
527
+ }
528
+
529
+ getRaw( name: string ) : string {
530
+ return this.m_data[name];
531
+ }
532
+
533
+ setRaw( name: string, data: string ) {
534
+ this.m_data[name] = data;
535
+ }
536
+
537
+ clone( data: any ) {
538
+ return new AutoRecord( {...data} );
539
+ }
540
+ }
541
+
542
+ /**
543
+ *
544
+ */
545
+
546
+ interface DataEventMap extends ComponentEvents {
547
+ change?: EvChange;
548
+ }
549
+
550
+ type DataSolver = ( data: any ) => Record[];
551
+
552
+ export interface DataProxyProps {
553
+ url: string;
554
+ params?: string[];
555
+ solver?: DataSolver;
556
+ }
557
+
558
+ export class DataProxy extends CoreElement<DataEventMap> {
559
+
560
+ protected m_props: DataProxyProps;
561
+
562
+ constructor( props: DataProxyProps ) {
563
+ super( );
564
+
565
+ this.m_props = props;
566
+ }
567
+
568
+ async load( url?: string ) {
569
+ if( url ) {
570
+ this.m_props.url = url;
571
+ }
572
+ else {
573
+ url = this.m_props.url;
574
+ }
575
+
576
+ if( this.m_props.params ) {
577
+ url += '?' + this.m_props.params.join( '&' );
578
+ }
579
+
580
+ const r = await fetch( url );
581
+ if( r.ok ) {
582
+ const raw = await r.json( );
583
+
584
+ let json = raw;
585
+ if( this.m_props.solver ) {
586
+ json = this.m_props.solver( json );
587
+ }
588
+
589
+ this.fire( 'change', {value:json,context:raw} );
590
+ }
591
+ }
592
+ }
593
+
594
+
595
+ /**
596
+ *
597
+ */
598
+
599
+ interface DataStoreProps<T extends Record> {
600
+ model: T;
601
+ data?: T[];
602
+ url?: string;
603
+ autoload?: false;
604
+ solver?: DataSolver;
605
+ }
606
+
607
+
608
+ interface DataStoreEventMap extends EventMap {
609
+ data_change: EvDataChange;
610
+ }
611
+
612
+
613
+
614
+ /**
615
+ *
616
+ */
617
+
618
+ export class DataStore<T extends Record = Record> extends EventSource<DataStoreEventMap> {
619
+
620
+ protected m_model: T;
621
+ protected m_fields: FieldInfo[];
622
+ protected m_records: T[];
623
+
624
+ protected m_proxy: DataProxy;
625
+ protected m_rec_index: DataIndex;
626
+
627
+ constructor(props: DataStoreProps<T> ) {
628
+ super( );
629
+
630
+ this.m_fields = undefined;
631
+ this.m_records = [];
632
+ this.m_rec_index = null;
633
+ this.m_model = props.model;
634
+ this.m_fields = props.model.getFields();
635
+
636
+ if (props.data) {
637
+ this.setRawData( props.data );
638
+ }
639
+ else if( props.url ) {
640
+ this.m_proxy = new DataProxy( {
641
+ url: props.url,
642
+ solver: props.solver,
643
+ });
644
+
645
+ this.m_proxy.on( 'change', ( ev: EvChange) => {
646
+ this.setData( ev.value );
647
+ });
648
+
649
+ if( props.autoload!=false ) {
650
+ this.m_proxy.load( );
651
+ }
652
+ }
653
+ }
654
+
655
+ /**
656
+ *
657
+ * @param records
658
+ */
659
+
660
+ async load( url?: string ) {
661
+ return this.m_proxy.load( url );
662
+ }
663
+
664
+ async reload( ) {
665
+ return this.m_proxy.load( );
666
+ }
667
+
668
+ /**
669
+ * convert raw objects to real records from model
670
+ * @param records
671
+ */
672
+
673
+ public setData( records: any[] ) {
674
+
675
+ let realRecords: T[] = [];
676
+
677
+ records.forEach( (rec) => {
678
+ realRecords.push( this.m_model.clone(rec) );
679
+ });
680
+
681
+ this.setRawData( realRecords );
682
+ }
683
+
684
+ /**
685
+ * just set the records
686
+ * @param records - must be of the same type as model
687
+ */
688
+
689
+ public setRawData(records: T[]) {
690
+
691
+ this.m_records = records;
692
+ this._rebuildIndex( );
693
+ this.fire( 'data_change', { change_type: 'change'} );
694
+ }
695
+
696
+
697
+ private _rebuildIndex( ) {
698
+ this.m_rec_index = null; // null to signal that we have to run on records instead of index
699
+ this.m_rec_index = this.createIndex( null ); // prepare index (remove deleted)
700
+ this.m_rec_index = this.sortIndex( this.m_rec_index, null ); // sort by id
701
+ }
702
+
703
+ /**
704
+ *
705
+ */
706
+
707
+ public update( rec: T ) {
708
+ let id = rec.getID();
709
+ let index = this.indexOfId(id);
710
+ if (index < 0) {
711
+ return false;
712
+ }
713
+
714
+ this.m_records[this.m_rec_index[index]] = rec;
715
+ this.fire( 'data_change', {change_type: 'update', id } );
716
+ return true;
717
+ }
718
+
719
+ /**
720
+ *
721
+ * @param data
722
+ */
723
+
724
+ public append( rec: T | any ) {
725
+
726
+ if( !(rec instanceof Record) ) {
727
+ let nrec = this.m_model.clone( );
728
+ rec = nrec.unSerialize( rec );
729
+ }
730
+
731
+ console.assert( rec.getID() );
732
+
733
+ this.m_records.push( rec );
734
+ this._rebuildIndex( );
735
+ this.fire( 'data_change', {change_type: 'create', id: rec.getID() } );
736
+ }
737
+
738
+ /**
739
+ *
740
+ */
741
+
742
+ getMaxId( ) {
743
+ let maxID: number = undefined;
744
+ this.m_records.forEach( (r) => {
745
+ let rid = r.getID( );
746
+ if( maxID===undefined || maxID<rid ) {
747
+ maxID = rid;
748
+ }
749
+ });
750
+
751
+ return maxID;
752
+ }
753
+
754
+ /**
755
+ *
756
+ * @param id
757
+ */
758
+
759
+ public delete(id: any): boolean {
760
+
761
+ let idx = this.indexOfId( id );
762
+ if( idx<0 ) {
763
+ return false;
764
+ }
765
+
766
+ idx = this.m_rec_index[idx];
767
+
768
+ // mark as deleted
769
+ this.m_records.splice( idx, 1 );
770
+ this._rebuildIndex( );
771
+ this.fire( 'data_change', { change_type: 'delete', id } );
772
+ return true;
773
+ }
774
+
775
+ /**
776
+ * return the number of records
777
+ */
778
+
779
+ get count( ) : number {
780
+ return this.m_rec_index ? this.m_rec_index.length : this.m_records.length;
781
+ }
782
+
783
+ /**
784
+ * return the fields
785
+ */
786
+
787
+ get fields( ) : FieldInfo [] {
788
+ return this.m_fields;
789
+ }
790
+
791
+ /**
792
+ * find the index of the element with the given id
793
+ */
794
+
795
+ public indexOfId(id: any ): number {
796
+
797
+ //if( this.count<10 ) {
798
+ // this.forEach( (rec) => rec.getID() == id );
799
+ //}
800
+
801
+ for( let lim = this.count, base = 0; lim != 0; lim >>= 1 ) {
802
+
803
+ let p = base + (lim >> 1); // int conversion
804
+ let idx = this.m_rec_index[p];
805
+ let rid = this.m_records[idx].getID( );
806
+
807
+ if( rid==id ) {
808
+ return p;
809
+ }
810
+
811
+ if( rid<id ) {
812
+ base = p+1;
813
+ lim--;
814
+ }
815
+ }
816
+
817
+ return -1;
818
+ }
819
+
820
+ /**
821
+ * return the record by it's id
822
+ * @returns record or null
823
+ */
824
+
825
+ public getById(id: any): T {
826
+ let idx = this.indexOfId( id );
827
+ if( idx<0 ) {
828
+ return null;
829
+ }
830
+
831
+ idx = this.m_rec_index[idx];
832
+ return this.m_records[idx];
833
+ }
834
+
835
+ /**
836
+ * return a record by it's index
837
+ * @returns record or null
838
+ */
839
+
840
+ public getByIndex( index: number ): T {
841
+ let idx = this.m_rec_index[index];
842
+ return this._getRecord( idx );
843
+ }
844
+
845
+ private _getRecord( index: number ) : T {
846
+ return this.m_records[index] ?? null;
847
+ }
848
+
849
+ public moveTo( other: DataStore<T> ) {
850
+ other.setRawData( this.m_records );
851
+ }
852
+
853
+ /**
854
+ * create a new view on the DataStore
855
+ * @param opts
856
+ */
857
+
858
+ createView( opts?: DataViewProps<T> ) : DataView<T> {
859
+ let eopts = { ...opts, store: this };
860
+ return new DataView( eopts );
861
+ }
862
+
863
+ /**
864
+ *
865
+ */
866
+
867
+ createIndex( filter: FilterInfo ) : DataIndex {
868
+
869
+ if( filter && filter.op==='empty-result' ) {
870
+ return new Uint32Array(0);
871
+ }
872
+
873
+ let index = new Uint32Array( this.m_records.length );
874
+ let iidx = 0;
875
+
876
+ if( !filter ) {
877
+ // reset filter
878
+ this.forEach( (rec, idx) => {
879
+ index[iidx++] = idx;
880
+ } );
881
+ }
882
+ else {
883
+ if( typeof(filter.op)==='function' ) {
884
+
885
+ let fn = <FilterFunc>filter.op;
886
+
887
+ // scan all records and append only interesting ones
888
+ this.forEach( (rec, idx) => {
889
+
890
+ // skip deleted
891
+ if( !rec ) {
892
+ return;
893
+ }
894
+
895
+ if( fn(rec) ) {
896
+ index[iidx++] = idx;
897
+ }
898
+ } );
899
+ }
900
+ else {
901
+ let filterFld = this.m_model.getFieldIndex( filter.field ); // field index to filter on
902
+ if( filterFld<0 ) {
903
+ // unknown filter field, nothing inside
904
+ console.assert( false, 'unknown field name in filter' )
905
+ return new Uint32Array(0);
906
+ }
907
+
908
+ let filterValue = filter.value;
909
+ if( isString(filterValue) && !filter.caseSensitive ) {
910
+ filterValue = filterValue.toUpperCase( );
911
+ }
912
+
913
+ function _lt( recval: string ) : boolean {
914
+ return recval < filterValue;
915
+ }
916
+
917
+ function _le( recval: string ) : boolean {
918
+ return recval <= filterValue;
919
+ }
920
+
921
+ function _eq( recval: string ) : boolean {
922
+ return recval == filterValue;
923
+ }
924
+
925
+ function _neq( recval: string ) : boolean {
926
+ return recval != filterValue;
927
+ }
928
+
929
+ function _ge( recval: string ) : boolean {
930
+ return recval >= filterValue;
931
+ }
932
+
933
+ function _gt( recval: string ) : boolean {
934
+ return recval > filterValue;
935
+ }
936
+
937
+ function _re( recval: string ) : boolean {
938
+ filterRe.lastIndex = -1;
939
+ return filterRe.test( recval );
940
+ }
941
+
942
+ let filterFn: ( rec: string ) => boolean; // filter fn
943
+ let filterRe: RegExp; // if fielter is regexp
944
+ if( filterValue instanceof RegExp ) {
945
+ filterRe = filterValue;
946
+ filterFn = _re;
947
+ }
948
+ else {
949
+ switch( filter.op ) {
950
+ case '<': { filterFn = _lt; break; }
951
+ case '<=': { filterFn = _le; break; }
952
+ case '=': { filterFn = _eq; break; }
953
+ case '>=': { filterFn = _ge; break; }
954
+ case '>': { filterFn = _gt; break; }
955
+ case '<>': { filterFn = _neq; break; }
956
+ }
957
+ }
958
+
959
+ // scan all records and append only interesting ones
960
+ this.forEach( (rec, idx) => {
961
+
962
+ // skip deleted
963
+ if( !rec ) {
964
+ return;
965
+ }
966
+
967
+ let field = rec.getRaw( filterFld );
968
+ if( field===null || field===undefined ) {
969
+ field = '';
970
+ }
971
+ else {
972
+ field = ''+field;
973
+ if( !filter.caseSensitive ) {
974
+ field = field.toUpperCase( );
975
+ }
976
+ }
977
+
978
+ let keep = filterFn( field );
979
+ if( keep ) {
980
+ index[iidx++] = idx;
981
+ };
982
+ });
983
+ }
984
+ }
985
+
986
+ return index.slice( 0, iidx );
987
+ }
988
+
989
+ sortIndex( index: DataIndex, sort: SortProp[] ) {
990
+
991
+ interface sort_info {
992
+ fidx: number,
993
+ asc: boolean
994
+ }
995
+
996
+ let bads = 0; // unknown fields
997
+ let fidxs: sort_info[] = []; // fields indexes
998
+
999
+ // if no fields are given, reset sort by id
1000
+ if ( sort===null ) {
1001
+ fidxs.push( { fidx: 0, asc: true } );
1002
+ }
1003
+ else {
1004
+ fidxs = sort.map( (si) => {
1005
+
1006
+ let fi = this.m_model.getFieldIndex( si.field );
1007
+ if (fi == -1) {
1008
+ console.assert( false, 'unknown field name in sort' )
1009
+ bads++;
1010
+ }
1011
+
1012
+ return { fidx: fi, asc: si.ascending };
1013
+ });
1014
+ }
1015
+
1016
+ // unknown field or nothing to sort on ??
1017
+ if( bads || fidxs.length==0 ) {
1018
+ return index;
1019
+ }
1020
+
1021
+ // sort only by one field : optimize it
1022
+ if( fidxs.length==1 ) {
1023
+
1024
+ let field = fidxs[0].fidx;
1025
+ index.sort( ( ia, ib ) => {
1026
+
1027
+ let va = this.getByIndex(ia).getRaw( field ) ?? '';
1028
+ let vb = this.getByIndex(ib).getRaw( field ) ?? '';
1029
+ if (va > vb) { return 1; }
1030
+ if (va < vb) { return -1; }
1031
+ return 0;
1032
+ } );
1033
+
1034
+ // just reverse if
1035
+ if( !fidxs[0].asc ) {
1036
+ index.reverse( );
1037
+ }
1038
+ }
1039
+ else {
1040
+ index.sort( ( ia, ib ) => {
1041
+
1042
+ for( let fi=0; fi<fidxs.length; fi++ ) {
1043
+
1044
+ let fidx = fidxs[fi].fidx;
1045
+ let mul = fidxs[fi].asc ? 1 : -1;
1046
+
1047
+ let va = this.getByIndex(ia).getRaw( fidx ) ?? '';
1048
+ let vb = this.getByIndex(ib).getRaw( fidx ) ?? '';
1049
+ if (va > vb) { return mul; }
1050
+ if (va < vb) { return -mul; }
1051
+ }
1052
+
1053
+ return 0;
1054
+ } );
1055
+ }
1056
+
1057
+ return index
1058
+ }
1059
+
1060
+ /**
1061
+ *
1062
+ */
1063
+
1064
+ forEach( cb: ( rec:T, index: number ) => any ) {
1065
+
1066
+ if( this.m_rec_index ) {
1067
+ this.m_rec_index.some( (ri,index) => {
1068
+ if( cb( this.m_records[ri], index ) ) {
1069
+ return index;
1070
+ }
1071
+ });
1072
+ }
1073
+ else {
1074
+ this.m_records.some( ( rec, index ) => {
1075
+ if( rec ) {
1076
+ if( cb( rec, index ) ) {
1077
+ return index;
1078
+ }
1079
+ }
1080
+ } );
1081
+ }
1082
+ }
1083
+
1084
+ export( ) {
1085
+ return this.m_records;
1086
+ }
1087
+
1088
+ changed( ) {
1089
+ this.fire( 'data_change', { change_type: 'change'} );
1090
+ }
1091
+ }
1092
+
1093
+
1094
+ // :: VIEWS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1095
+
1096
+ export interface EvViewChange extends CoreEvent {
1097
+ change_type: "change" | "filter" | "sort";
1098
+ }
1099
+
1100
+ interface DataViewEventMap extends EventMap {
1101
+ view_change: EvViewChange;
1102
+ }
1103
+
1104
+ interface DataViewProps<T extends Record> {
1105
+ store?: DataStore<T>;
1106
+ filter?: FilterInfo;
1107
+ order?: string | SortProp[] | SortProp;
1108
+ }
1109
+
1110
+ export type FilterFunc = ( rec: Record ) => boolean;
1111
+
1112
+ export interface FilterInfo {
1113
+ op: '<' | '<=' | '=' | '>=' | '>' | '<>' | 'empty-result' | FilterFunc, // emptydb mean return an empty result always
1114
+ field?: string;
1115
+ value?: string | RegExp; // if regexp then operator is =
1116
+ caseSensitive?: boolean;
1117
+ }
1118
+
1119
+
1120
+ export interface SortProp {
1121
+ field: string; //
1122
+ ascending: boolean; //
1123
+ }
1124
+
1125
+
1126
+
1127
+ /**
1128
+ * Dataview allow different views of the DataStore.
1129
+ * You can sort the columns & filter data
1130
+ * You can have multiple views for a single DataStore
1131
+ */
1132
+
1133
+ export class DataView<T extends Record = Record> extends CoreElement<DataViewEventMap>
1134
+ {
1135
+ protected m_index: DataIndex;
1136
+ protected m_store: DataStore<T>;
1137
+
1138
+ protected m_sort: SortProp[];
1139
+ protected m_filter: FilterInfo;
1140
+
1141
+ protected m_props: DataViewProps<T>;
1142
+
1143
+ constructor( props: DataViewProps<T> ) {
1144
+ super( );
1145
+
1146
+ this.m_props = props;
1147
+ this.m_store = props.store;
1148
+ this.m_index = null;
1149
+ this.m_filter = null;
1150
+ this.m_sort = null;
1151
+
1152
+ this.filter( props.filter );
1153
+
1154
+ if( props.order ) {
1155
+ if( isString(props.order) ) {
1156
+ this.sort( [ { field: props.order, ascending: true } ] );
1157
+ }
1158
+ else if( isArray(props.order) ) {
1159
+ this.sort( props.order );
1160
+ }
1161
+ else {
1162
+ this.sort( [props.order] );
1163
+ }
1164
+ }
1165
+ else {
1166
+ this.sort( null );
1167
+ }
1168
+
1169
+ this.m_store.addListener( 'data_change', ( e ) => this._storeChange(e) );
1170
+ }
1171
+
1172
+ private _storeChange( ev: EvDataChange ) {
1173
+
1174
+ this._filter( this.m_filter, ev.type!='change' );
1175
+ this._sort( this.m_sort, ev.type!='change' );
1176
+
1177
+ this.fire( 'view_change', { change_type: 'change' } );
1178
+ }
1179
+
1180
+ /**
1181
+ *
1182
+ * @param filter
1183
+ */
1184
+
1185
+ public filter( filter?: FilterInfo ) : number {
1186
+
1187
+ this.m_index = null; // null to signal that we have to run on records instead of index
1188
+ return this._filter( filter, true );
1189
+ }
1190
+
1191
+ private _filter( filter: FilterInfo, notify: boolean) : number {
1192
+
1193
+ this.m_index = this.m_store.createIndex( filter );
1194
+ this.m_filter = filter;
1195
+
1196
+ // need to sort again:
1197
+ if( this.m_sort ) {
1198
+ this.sort( this.m_sort );
1199
+ }
1200
+
1201
+ if( notify ) {
1202
+ this.fire( 'view_change', { change_type: 'filter' } );
1203
+ }
1204
+
1205
+ return this.m_index.length;
1206
+ }
1207
+
1208
+ /**
1209
+ *
1210
+ * @param columns
1211
+ * @param ascending
1212
+ */
1213
+
1214
+ public sort( props: SortProp[] ) {
1215
+ this._sort( props, true );
1216
+ }
1217
+
1218
+ private _sort( props: SortProp[], notify: boolean ) {
1219
+ this.m_index = this.m_store.sortIndex( this.m_index, props );
1220
+ this.m_sort = props;
1221
+
1222
+ if( notify ) {
1223
+ this.fire( 'view_change', { change_type: 'sort' } );
1224
+ }
1225
+ }
1226
+
1227
+ /**
1228
+ *
1229
+ */
1230
+
1231
+ getStore ( ) {
1232
+ return this.m_store;
1233
+ }
1234
+
1235
+ /**
1236
+ *
1237
+ */
1238
+
1239
+ public getCount() {
1240
+ return this.m_index.length;
1241
+ }
1242
+
1243
+ /**
1244
+ *
1245
+ * @param id
1246
+ */
1247
+
1248
+ public indexOfId(id: any): number {
1249
+ let ridx = this.m_store.indexOfId( id );
1250
+ return this.m_index.findIndex( (rid) => rid === ridx );
1251
+ }
1252
+
1253
+ /**
1254
+ *
1255
+ * @param index
1256
+ */
1257
+
1258
+ public getByIndex(index: number): T {
1259
+
1260
+ if (index >= 0 && index < this.m_index.length) {
1261
+ let rid = this.m_index[index];
1262
+ return this.m_store.getByIndex( rid );
1263
+ }
1264
+
1265
+ return null;
1266
+ }
1267
+
1268
+ /**
1269
+ *
1270
+ * @param id
1271
+ */
1272
+
1273
+ public getById( id: any): T {
1274
+ return this.m_store.getById( id );
1275
+ }
1276
+
1277
+ changed( ) {
1278
+ this.fire( 'view_change', {change_type:'change'} );
1279
+ }
1280
+
1281
+ /**
1282
+ *
1283
+ */
1284
+
1285
+ forEach( cb: ( rec:T, index: number ) => any ) {
1286
+ this.m_index.some( ( index ) => {
1287
+ let rec = this.m_store.getByIndex( index );
1288
+ if( rec ) {
1289
+ if( cb( rec, index ) ) {
1290
+ return index;
1291
+ }
1292
+ }
1293
+ } );
1294
+ }
1295
+ }
1296
+
1297
+
1298
+