x4js 2.3.18 → 2.3.20

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 (38) hide show
  1. package/aicli.md +3 -3
  2. package/aicontext.md +51 -23
  3. package/cli/config.mjs +14 -7
  4. package/cli/copy-plugin.mjs +8 -0
  5. package/cli/create.mjs +8 -2
  6. package/cli/dev.mjs +4 -1
  7. package/cli/less-plugin.mjs +5 -1
  8. package/cli/main.mjs +15 -5
  9. package/cli/rawfile-plugin.mjs +23 -3
  10. package/cli/templates.mjs +5 -1
  11. package/package.json +1 -1
  12. package/src/components/calendar/calendar.module.scss +16 -1
  13. package/src/components/calendar/calendar.ts +52 -9
  14. package/src/components/checkbox/checkbox.ts +1 -0
  15. package/src/components/combobox/combobox.ts +51 -23
  16. package/src/components/dialog/dialog.ts +4 -1
  17. package/src/components/form/form.ts +9 -5
  18. package/src/components/gridview/gridview.ts +2 -1
  19. package/src/components/input/input.ts +63 -38
  20. package/src/components/keyboard/keyboard.module.scss +35 -4
  21. package/src/components/keyboard/keyboard.ts +120 -21
  22. package/src/components/listbox/listbox.ts +6 -1
  23. package/src/components/messages/messages.ts +12 -3
  24. package/src/components/popup/popup.ts +19 -20
  25. package/src/components/radio/radio.ts +8 -4
  26. package/src/components/rating/rating.ts +38 -48
  27. package/src/components/slider/slider.ts +38 -8
  28. package/src/components/spreadsheet/spreadsheet.ts +140 -131
  29. package/src/components/switch/switch.ts +2 -1
  30. package/src/components/textarea/textarea.module.scss +2 -4
  31. package/src/components/textarea/textarea.ts +2 -1
  32. package/src/components/textedit/textedit.ts +12 -2
  33. package/src/core/core_application.ts +8 -4
  34. package/src/core/core_data.ts +64 -26
  35. package/src/core/core_dom.ts +9 -1
  36. package/src/core/core_pdf.ts +1200 -46
  37. package/src/core/core_router.ts +42 -44
  38. package/src/core/core_state.ts +22 -5
@@ -348,6 +348,11 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
348
348
  this.lock(false);
349
349
  });
350
350
 
351
+ // out of the document: nothing is drawn, as before its first insertion
352
+ this.addDOMEvent("removed", () => {
353
+ this.lock(true);
354
+ });
355
+
351
356
  this.addDOMEvent("resized", () => {
352
357
  this._updateFlexs();
353
358
  this._computeFullSize();
@@ -358,6 +363,139 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
358
363
  this._on_key(e);
359
364
  })
360
365
 
366
+ // the handlers of the grid itself are put once, here. _init builds what is
367
+ // inside the grid, each time it comes in the document
368
+
369
+ // WHEEL
370
+ this.addDOMEvent("wheel", (ev: WheelEvent) => {
371
+ // many rows: one pixel of the scroll bar is one row, wherever the pointer is
372
+ if (ev.deltaY && this._store && this._store.getRowCount() >= SCROLL_LIMIT) {
373
+ this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -1 : 1);
374
+ ev.stopPropagation();
375
+ ev.preventDefault();
376
+ }
377
+ else if (this._has_fixed && ev.deltaY) {
378
+ // wheel on fixed part
379
+ // fixed part do not have scrollbar, so we need to handle it by hand
380
+ let t = ev.target as Node;
381
+ while (t != this.dom) {
382
+ if (t == this._vheader.dom) {
383
+ this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -this._row_height : this._row_height);
384
+ ev.stopPropagation();
385
+ ev.preventDefault();
386
+ break;
387
+ }
388
+
389
+ t = t.parentNode;
390
+ }
391
+ }
392
+ })
393
+
394
+ const targetCell = (e: MouseEvent) => {
395
+ let el = e.target as Element;
396
+ while (el && !el.classList.contains("cell")) {
397
+ el = el.parentElement;
398
+ }
399
+
400
+ if (el) {
401
+ const cel = componentFromDOM(el);
402
+ return {
403
+ ref: cel.getIntData("ref"),
404
+ row: cel.getInternalData("row"),
405
+ col: cel.getInternalData("col"),
406
+ }
407
+ }
408
+
409
+ return undefined;
410
+ }
411
+
412
+
413
+ // MOUSEDOWN
414
+ this.addDOMEvent("mousedown", (e) => {
415
+ if( !this._edit || this._inEditor(e) ) {
416
+ return;
417
+ }
418
+
419
+ // click on another cell while editing:
420
+ // validate now, rows may be rebuilt before the click event (and the click lost)
421
+ const ref = targetCell(e);
422
+ if( ref && ref.row!==undefined ) {
423
+ this._endEdit( true, true );
424
+
425
+ if (!this._selection.has(ref.ref)) {
426
+ this._clearSelection( false );
427
+ this._addSelection(ref.ref,true);
428
+ }
429
+
430
+ e.preventDefault( );
431
+ }
432
+ });
433
+
434
+ // CLICK
435
+ this.addDOMEvent("click", (e) => {
436
+ if( this._inEditor(e) ) {
437
+ return;
438
+ }
439
+
440
+ const ref = targetCell(e);
441
+ if (ref) {
442
+ //TODO: multiselection
443
+ if (!this._selection.has(ref.ref)) {
444
+ this._clearSelection( false );
445
+ this._addSelection(ref.ref,true);
446
+ }
447
+ }
448
+ });
449
+
450
+ // DBLCLICK
451
+ this.addDOMEvent("dblclick", (e) => {
452
+ if( this._inEditor(e) ) {
453
+ return;
454
+ }
455
+
456
+ const ref = targetCell(e);
457
+ if (ref) {
458
+ //TODO: multiselection
459
+ if (!this._selection.has(ref.ref)) {
460
+ this._clearSelection(false);
461
+ this._addSelection(ref.ref,true);
462
+ }
463
+
464
+ const dev: EvDblClick = { context: { row: ref.row, col: ref.col } };
465
+ this.fire( "dblClick", dev );
466
+
467
+ // checkboxes are toggled, other cells are edited
468
+ if( !dev.defaultPrevented && !this._toggleCell( ref.row, ref.col ) ) {
469
+ this.editCell( ref.row, ref.col );
470
+ }
471
+ }
472
+ });
473
+
474
+ // CONTEXT
475
+ this.addDOMEvent("contextmenu", (e) => {
476
+ // keep the native menu of the editor (copy/paste)
477
+ if( this._inEditor(e) ) {
478
+ return;
479
+ }
480
+
481
+ const ref = targetCell(e);
482
+ if (ref) {
483
+ //TODO: multiselection
484
+ if (!this._selection.has(ref.ref)) {
485
+ this._clearSelection( false );
486
+ this._addSelection(ref.ref, true );
487
+ }
488
+
489
+ this.fire( "contextMenu", { uievent: e, context: { row: ref.row, col: ref.col } } );
490
+ }
491
+ else {
492
+ this.fire( "contextMenu", { uievent: e, context: null } );
493
+ }
494
+
495
+ e.preventDefault();
496
+ e.stopPropagation();
497
+ });
498
+
361
499
  if (props.store) {
362
500
  this.setStore(props.store);
363
501
  }
@@ -643,7 +781,8 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
643
781
  if (lock) {
644
782
  this._lock++;
645
783
  }
646
- else {
784
+ // an unlock too many leaves it at 0: the grid goes on drawing
785
+ else if (this._lock > 0) {
647
786
  if (--this._lock == 0 && this._dirty) {
648
787
  this._update(true);
649
788
  }
@@ -1121,136 +1260,6 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
1121
1260
  this._update()
1122
1261
  });
1123
1262
 
1124
- // WHEEL
1125
- this.addDOMEvent("wheel", (ev: WheelEvent) => {
1126
- if (ev.deltaY && this._store && this._store.getRowCount() >= SCROLL_LIMIT) {
1127
- this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -1 : 1);
1128
- ev.stopPropagation();
1129
- ev.preventDefault();
1130
- }
1131
-
1132
- if (this._has_fixed && ev.deltaY) {
1133
- // wheel on fixed part
1134
- // fixed part do not have scrollbar, so we need to handle it by hand
1135
- let t = ev.target as Node;
1136
- while (t != this.dom) {
1137
- if (t == this._vheader.dom) {
1138
- this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -this._row_height : this._row_height);
1139
- ev.stopPropagation();
1140
- ev.preventDefault();
1141
- break;
1142
- }
1143
-
1144
- t = t.parentNode;
1145
- }
1146
- }
1147
- })
1148
-
1149
- const targetCell = (e: MouseEvent) => {
1150
- let el = e.target as Element;
1151
- while (el && !el.classList.contains("cell")) {
1152
- el = el.parentElement;
1153
- }
1154
-
1155
- if (el) {
1156
- const cel = componentFromDOM(el);
1157
- return {
1158
- ref: cel.getIntData("ref"),
1159
- row: cel.getInternalData("row"),
1160
- col: cel.getInternalData("col"),
1161
- }
1162
- }
1163
-
1164
- return undefined;
1165
- }
1166
-
1167
-
1168
- // MOUSEDOWN
1169
- this.addDOMEvent("mousedown", (e) => {
1170
- if( !this._edit || this._inEditor(e) ) {
1171
- return;
1172
- }
1173
-
1174
- // click on another cell while editing:
1175
- // validate now, rows may be rebuilt before the click event (and the click lost)
1176
- const ref = targetCell(e);
1177
- if( ref && ref.row!==undefined ) {
1178
- this._endEdit( true, true );
1179
-
1180
- if (!this._selection.has(ref.ref)) {
1181
- this._clearSelection( false );
1182
- this._addSelection(ref.ref,true);
1183
- }
1184
-
1185
- e.preventDefault( );
1186
- }
1187
- });
1188
-
1189
- // CLICK
1190
- this.addDOMEvent("click", (e) => {
1191
- if( this._inEditor(e) ) {
1192
- return;
1193
- }
1194
-
1195
- const ref = targetCell(e);
1196
- if (ref) {
1197
- //TODO: multiselection
1198
- if (!this._selection.has(ref.ref)) {
1199
- this._clearSelection( false );
1200
- this._addSelection(ref.ref,true);
1201
- }
1202
- }
1203
- });
1204
-
1205
- // DBLCLICK
1206
- this.addDOMEvent("dblclick", (e) => {
1207
- if( this._inEditor(e) ) {
1208
- return;
1209
- }
1210
-
1211
- const ref = targetCell(e);
1212
- if (ref) {
1213
- //TODO: multiselection
1214
- if (!this._selection.has(ref.ref)) {
1215
- this._clearSelection(false);
1216
- this._addSelection(ref.ref,true);
1217
- }
1218
-
1219
- const dev: EvDblClick = { context: { row: ref.row, col: ref.col } };
1220
- this.fire( "dblClick", dev );
1221
-
1222
- // checkboxes are toggled, other cells are edited
1223
- if( !dev.defaultPrevented && !this._toggleCell( ref.row, ref.col ) ) {
1224
- this.editCell( ref.row, ref.col );
1225
- }
1226
- }
1227
- });
1228
-
1229
- // CONTEXT
1230
- this.addDOMEvent("contextmenu", (e) => {
1231
- // keep the native menu of the editor (copy/paste)
1232
- if( this._inEditor(e) ) {
1233
- return;
1234
- }
1235
-
1236
- const ref = targetCell(e);
1237
- if (ref) {
1238
- //TODO: multiselection
1239
- if (!this._selection.has(ref.ref)) {
1240
- this._clearSelection( false );
1241
- this._addSelection(ref.ref, true );
1242
- }
1243
-
1244
- this.fire( "contextMenu", { uievent: e, context: { row: ref.row, col: ref.col } } );
1245
- }
1246
- else {
1247
- this.fire( "contextMenu", { uievent: e, context: null } );
1248
- }
1249
-
1250
- e.preventDefault();
1251
- e.stopPropagation();
1252
- });
1253
-
1254
1263
  this._updateFlexs();
1255
1264
 
1256
1265
  this._fheader = this._buildColHeader(true);
@@ -53,7 +53,8 @@ export class Switch extends HBox<SwitchProps> {
53
53
  new Component( {
54
54
  cls: "switch",
55
55
  content: [
56
- new Input( { type: "checkbox", id: inputId, name: props.name, checked: props.checked } ),
56
+ // value: sent by a <form> of the browser when checked; a Form of x4 reads a boolean
57
+ new Input( { type: "checkbox", id: inputId, name: props.name, value: props.value, checked: props.checked } ),
57
58
  new Component( { cls: "track" } ),
58
59
  new Component( { cls: "thumb" } ),
59
60
  ]
@@ -24,6 +24,8 @@
24
24
 
25
25
  .x4simpletextarea {
26
26
 
27
+ flex-shrink: 0; // alone in a box, it keeps its height: the box does not crush it
28
+ resize: none; // no handle unless the prop resize asks for one
27
29
  padding: 4px;
28
30
  outline: none;
29
31
  font: inherit;
@@ -44,10 +46,6 @@
44
46
  color: var( --input-placeholder );
45
47
  font-style: italic;
46
48
  }
47
-
48
- &[resize=false] {
49
- resize: none;
50
- }
51
49
  }
52
50
 
53
51
  .x4textarea {
@@ -63,8 +63,9 @@ export class SimpleTextArea extends Component<SimpleTextAreaProps,TextAreaEvents
63
63
  this.setText( props.value+'' );
64
64
  }
65
65
 
66
+ // none by default (the stylesheet); true: "both", false: "none"
66
67
  if( props.resize!==undefined ) {
67
- this.setStyleValue( "resize", props.resize===false ? "none" : props.resize as string );
68
+ this.setStyleValue( "resize", props.resize===true ? "both" : props.resize===false ? "none" : props.resize );
68
69
  }
69
70
 
70
71
  if( props.readonly ) {
@@ -87,7 +87,9 @@ export class TextEdit<E extends ComponentEvents = ComponentEvents> extends HBox<
87
87
  private input: Input;
88
88
 
89
89
  constructor( props: TextEditProps ) {
90
- super( props );
90
+ // dom_events are for the input (focus, blur, keydown...): not on the box too,
91
+ // where keydown would run a second time, and focus never (it does not bubble)
92
+ super( { ...props, dom_events: undefined } );
91
93
 
92
94
  if( !props.inputId ) {
93
95
  props.inputId = makeUniqueComponentId()
@@ -104,8 +106,16 @@ export class TextEdit<E extends ComponentEvents = ComponentEvents> extends HBox<
104
106
 
105
107
 
106
108
  const iprops: InputProps = { ...props, id: props.inputId, attrs: props.inputAttrs, width: props.inputWidth };
107
- // no propagation...
109
+
110
+ // the props of the box stay on it: given to the input too, a style would be put
111
+ // twice, a tooltip twice, the height of the box on the field...
108
112
  delete iprops.cls;
113
+ delete iprops.style;
114
+ delete iprops.tooltip;
115
+ delete iprops.hidden;
116
+ delete iprops.flex;
117
+ delete iprops.stretch;
118
+ delete iprops.height;
109
119
 
110
120
  this.setContent( [
111
121
  props.label ? new HBox( { id: "label", width: props.labelWidth, content: [
@@ -104,8 +104,10 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
104
104
  // eslint-disable-next-line @typescript-eslint/no-this-alias
105
105
  main_app = this;
106
106
 
107
+ this.props = props;
108
+
107
109
  const loaded = ( ) => {
108
- this.mount( props.mountPoint ?? 'body' )
110
+ this.mount( this.props.mountPoint )
109
111
  }
110
112
 
111
113
  if( document.readyState=='complete' ) {
@@ -141,13 +143,14 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
141
143
 
142
144
  /**
143
145
  * Sets the main view component for the application.
144
- * This component will be mounted to the DOM.
146
+ * This component will be mounted to the DOM, in `mountPoint`.
147
+ * Called once: each call sets up the keyboard (Tab, shortcuts) again.
145
148
  * @param view - The component to set as the main view.
146
149
  */
147
150
 
148
151
  setMainView( view: Component ) {
149
152
  this.mainview = view;
150
- this.mount( );
153
+ this.mount( this.props.mountPoint );
151
154
  this._setupKeyboard( );
152
155
  }
153
156
 
@@ -321,7 +324,8 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
321
324
  return;
322
325
  }
323
326
 
324
- if( msg_socket ) {
327
+ // send throws while the socket is connecting
328
+ if( msg_socket && opened ) {
325
329
  msg_socket.send( JSON.stringify( {
326
330
  msg: e.msg,
327
331
  params: e.params,
@@ -91,11 +91,8 @@ function _getMetas( obj: object, create = true ) : MetaInfos {
91
91
  mfld = Object.prototype.hasOwnProperty.call(ctor,metaFields) ? ctor[metaFields] : undefined;
92
92
  }
93
93
 
94
+ // a class without decorators of its own comes here too: its metas are made from its parent's
94
95
  if( mfld===undefined ) {
95
- if( !create && ctor!=DataModel ) {
96
- console.assert( mfld!==undefined );
97
- }
98
-
99
96
  // construct our metas
100
97
  mfld = new MetaInfos( ctor.name );
101
98
 
@@ -105,8 +102,6 @@ function _getMetas( obj: object, create = true ) : MetaInfos {
105
102
  if( pctor!=DataModel ) {
106
103
  let pmetas = pctor[metaFields];
107
104
  mfld.fields = [...pmetas.fields, ...mfld.fields ]
108
-
109
- console.assert( mfld.id===undefined, 'cannot define mutiple record id' );
110
105
  if( !mfld.id ) {
111
106
  mfld.id = pmetas.id;
112
107
  }
@@ -143,6 +138,7 @@ export namespace data {
143
138
  required: true,
144
139
  });
145
140
 
141
+ console.assert( !metas.id, 'cannot define multiple record id' ); // the parent class has one already
146
142
  metas.id = fldName;
147
143
  }
148
144
  }
@@ -371,7 +367,7 @@ export class DataModel<T = any> {
371
367
  });
372
368
 
373
369
  if( id!==undefined ) {
374
- rec[fields[0].name] = id;
370
+ rec[_getMetas( this, false ).id] = id; // the field of the id, as getID reads it
375
371
  }
376
372
  else {
377
373
  console.assert( this.getID(rec)!==undefined ); // store do not have ID field
@@ -517,6 +513,10 @@ export interface DataProxyProps {
517
513
  solver?: DataSolver;
518
514
  }
519
515
 
516
+ /**
517
+ * @deprecated the loading from an url is to be reviewed
518
+ */
519
+
520
520
  export class DataProxy extends CoreElement<DataEventMap> {
521
521
 
522
522
  protected m_props: DataProxyProps;
@@ -561,8 +561,11 @@ export class DataProxy extends CoreElement<DataEventMap> {
561
561
  interface DataStoreProps {
562
562
  model: DataModel;
563
563
  data?: any[];
564
+ /** @deprecated the loading from an url is to be reviewed: load the data, then setData */
564
565
  url?: string;
566
+ /** @deprecated see url */
565
567
  autoload?: false;
568
+ /** @deprecated see url */
566
569
  solver?: DataSolver;
567
570
  }
568
571
 
@@ -615,14 +618,15 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
615
618
  }
616
619
 
617
620
  /**
618
- *
619
- * @param records
621
+ * @deprecated the loading from an url is to be reviewed: load the data, then setData.
622
+ * Only for a store created with url and no data; an HTTP error is not reported
620
623
  */
621
624
 
622
625
  async load( url?: string ) {
623
626
  return this.m_proxy.load( url );
624
627
  }
625
628
 
629
+ /** @deprecated see load */
626
630
  async reload( ) {
627
631
  return this.m_proxy.load( );
628
632
  }
@@ -813,8 +817,13 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
813
817
  return this.m_records[index] ?? null;
814
818
  }
815
819
 
820
+ /**
821
+ * gives the records to other: this store is emptied, they do not share them
822
+ */
823
+
816
824
  public moveTo( other: DataStore ) {
817
825
  other.setRawData( this.m_records );
826
+ this.setRawData( [] );
818
827
  }
819
828
 
820
829
  /**
@@ -871,32 +880,32 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
871
880
  return new Uint32Array(0);
872
881
  }
873
882
 
874
- let filterValue = filter.value;
883
+ let filterValue: any = filter.value;
875
884
  if( isString(filterValue) && !filter.caseSensitive ) {
876
885
  filterValue = filterValue.toUpperCase( );
877
886
  }
878
-
879
- const _lt = ( recval: string ) : boolean => {
887
+
888
+ const _lt = ( recval: any ) : boolean => {
880
889
  return recval < filterValue;
881
890
  }
882
891
 
883
- const _le = ( recval: string ) : boolean => {
892
+ const _le = ( recval: any ) : boolean => {
884
893
  return recval <= filterValue;
885
894
  }
886
895
 
887
- const _eq = ( recval: string ) : boolean => {
896
+ const _eq = ( recval: any ) : boolean => {
888
897
  return recval == filterValue;
889
898
  }
890
899
 
891
- const _neq = ( recval: string ) : boolean => {
900
+ const _neq = ( recval: any ) : boolean => {
892
901
  return recval != filterValue;
893
902
  }
894
903
 
895
- const _ge = ( recval: string ) : boolean => {
904
+ const _ge = ( recval: any ) : boolean => {
896
905
  return recval >= filterValue;
897
906
  }
898
907
 
899
- const _gt = ( recval: string ) : boolean => {
908
+ const _gt = ( recval: any ) : boolean => {
900
909
  return recval > filterValue;
901
910
  }
902
911
 
@@ -905,9 +914,10 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
905
914
  return filterRe.test( recval );
906
915
  }
907
916
 
908
- let filterFn: ( rec: string ) => boolean; // filter fn
917
+ let filterFn: ( rec: any ) => boolean; // filter fn
909
918
  let filterRe: RegExp; // if fielter is regexp
910
919
  if( filterValue instanceof RegExp ) {
920
+ // tested on the field as it is: the regexp says if the case counts (flag i)
911
921
  filterRe = filterValue;
912
922
  filterFn = _re;
913
923
  }
@@ -921,7 +931,14 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
921
931
  case '<>': { filterFn = _neq; break; }
922
932
  }
923
933
  }
924
-
934
+
935
+ // int, float, date: compared as numbers, a date by its time
936
+ const type = this.m_fields[filterFld].type;
937
+ const numeric = !filterRe && (type==='int' || type==='float' || type==='date');
938
+ if( numeric ) {
939
+ filterValue = type==='date' ? new Date( filter.value as any ).getTime( ) : Number( filter.value );
940
+ }
941
+
925
942
  // scan all records and append only interesting ones
926
943
  const m = this.m_model;
927
944
 
@@ -933,12 +950,16 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
933
950
  }
934
951
 
935
952
  let field = m.getRaw( filterFld, rec );
936
- if( field===null || field===undefined ) {
953
+ if( numeric ) {
954
+ // +: a Date gives its time, without a new object. A null field: NaN, every comparison is false but <>
955
+ field = field===null || field===undefined ? NaN : +field;
956
+ }
957
+ else if( field===null || field===undefined ) {
937
958
  field = '';
938
959
  }
939
960
  else {
940
961
  field = ''+field;
941
- if( !filter.caseSensitive ) {
962
+ if( !filter.caseSensitive && !filterRe ) {
942
963
  field = field.toUpperCase( );
943
964
  }
944
965
  }
@@ -960,14 +981,17 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
960
981
  fidx: number,
961
982
  asc: boolean,
962
983
  cb: SortCallback,
984
+ numeric: boolean,
963
985
  }
964
986
 
965
987
  let bads = 0; // unknown fields
966
988
  let fidxs: sort_info[] = []; // fields indexes
967
989
 
968
- // if no fields are given, reset sort by id
990
+ // if no fields are given, reset sort by id: the field marked by data.id( ),
991
+ // which is not always the first one, and the order in which indexOfId searches
969
992
  if ( sort===null ) {
970
- fidxs.push( { fidx: 0, asc: true, cb: null } );
993
+ const id = _getMetas( this.m_model, false ).id;
994
+ fidxs.push( { fidx: this.m_model.getFieldIndex( id ), asc: true, cb: null, numeric: false } );
971
995
  }
972
996
  else {
973
997
  fidxs = sort.map( (si) => {
@@ -978,7 +1002,7 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
978
1002
  bads++;
979
1003
  }
980
1004
 
981
- return { fidx: fi, asc: si.ascending, cb: si.callback };
1005
+ return { fidx: fi, asc: si.ascending, cb: si.callback, numeric: si.numeric };
982
1006
  });
983
1007
  }
984
1008
 
@@ -994,6 +1018,7 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
994
1018
 
995
1019
  const field = fidxs[0].fidx;
996
1020
  const cb = fidxs[0].cb;
1021
+ const numeric = fidxs[0].numeric;
997
1022
 
998
1023
  index.sort( ( ia, ib ) => {
999
1024
 
@@ -1004,6 +1029,10 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
1004
1029
  return cb( va, vb );
1005
1030
  }
1006
1031
 
1032
+ if( numeric ) {
1033
+ return Number( va ) - Number( vb );
1034
+ }
1035
+
1007
1036
  if (va > vb) { return 1; }
1008
1037
  if (va < vb) { return -1; }
1009
1038
  return 0;
@@ -1024,8 +1053,17 @@ export class DataStore<T = any> extends EventSource<DataStoreEventMap> {
1024
1053
  let va = m.getRaw( fidx, this.getByIndex(ia) ) ?? '';
1025
1054
  let vb = m.getRaw( fidx, this.getByIndex(ib) ) ?? '';
1026
1055
 
1056
+ // a field that finds them equal gives the hand to the next one
1027
1057
  if( fi.cb ) {
1028
- return fi.cb( va, vb );
1058
+ const r = fi.cb( va, vb );
1059
+ if( r ) { return r*mul; }
1060
+ continue;
1061
+ }
1062
+
1063
+ if( fi.numeric ) {
1064
+ const r = Number( va ) - Number( vb );
1065
+ if( r ) { return r*mul; } // NaN, not a number: equal
1066
+ continue;
1029
1067
  }
1030
1068
 
1031
1069
  if (va > vb) { return mul; }
@@ -1121,7 +1159,7 @@ export type FilterFunc = ( rec: DataRecord ) => boolean;
1121
1159
  export interface FilterInfo {
1122
1160
  op: '<' | '<=' | '=' | '>=' | '>' | '<>' | 'empty-result' | FilterFunc, // emptydb mean return an empty result always
1123
1161
  field?: string;
1124
- value?: string | RegExp; // if regexp then operator is =
1162
+ value?: string | number | RegExp; // if regexp then operator is =
1125
1163
  caseSensitive?: boolean;
1126
1164
  }
1127
1165
 
@@ -38,12 +38,14 @@ const observeMutation = (mutations: MutationRecord[], observer: MutationObserver
38
38
  // moves are handled this way
39
39
  const added = new Set<Node>( );
40
40
  const removed = new Set<Node>( );
41
+ const moved = new Set<Node>( ); // out of their place and back in the document in the same task
41
42
 
42
43
  for( const mutation of mutations ) {
43
44
  if( mutation.type == "childList" ) {
44
45
  mutation.addedNodes.forEach( n => {
45
46
  if( removed.has( n ) ) {
46
- removed.delete( n );
47
+ removed.delete( n );
48
+ moved.add( n );
47
49
  }
48
50
  else {
49
51
  added.add( n );
@@ -70,6 +72,12 @@ const observeMutation = (mutations: MutationRecord[], observer: MutationObserver
70
72
 
71
73
  const notify = ( node: Node, create: boolean ) => {
72
74
 
75
+ // a node that was only moved was created before, as all that it holds,
76
+ // even when it is moved into a node that is new
77
+ if( create && moved.has( node ) ) {
78
+ return;
79
+ }
80
+
73
81
  if( create ) {
74
82
  sendEvent( node, "created" );
75
83
  }