x4js 2.0.35 → 2.1.0-manual

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