x4js 1.4.2

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