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,1316 +1,1276 @@
1
- /**
2
- * ___ ___ __
3
- * \ \/ / / _
4
- * \ / /_| |_
5
- * / \____ _|
6
- * /__/\__\ |_|
7
- *
8
- * @file gridview.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 { Component, ComponentContent, ComponentEvents, ComponentProps, EvClick, EvContextMenu, EvDblClick, EvSelectionChange } from '../../core/component';
19
- import { class_ns, isNumber, isString, setWaitCursor, UnsafeHtml } from '../../core/core_tools';
20
- import { DataModel, DataStore, DataView, DataRecord, EvViewChange, SortCallback } from '../../core/core_data';
21
- import { EventCallback } from '../../core/core_events';
22
- import { kbNav } from '../../core/core_tools';
23
-
24
- import { Icon } from '../icon/icon';
25
- import { Image } from '../image/image'
26
- import { Box } from '../boxes/boxes';
27
- import { CSizer } from '../sizers/sizer'
28
- import { Viewport } from '../viewport/viewport';
29
- import { Label, SimpleText } from '../label/label';
30
-
31
- import { _tr } from '../../core/core_i18n';
32
-
33
- import check_icon from "../checkbox/check.svg";
34
- import empty_icon from "./folder-open.svg";
35
-
36
- import "./gridview.module.scss"
37
-
38
- export type CellRenderer = (rec: DataRecord) => Component;
39
- export type CellClassifier = (data: any, rec: DataRecord, col: string ) => string; // return the cell computed class
40
-
41
- type ColType = "number" | "money" | "checkbox" | "date" | "string" | "image" | "percent" | "icon" | "date-time";
42
-
43
- const SCROLL_LIMIT = 200;
44
-
45
-
46
- /**
47
- *
48
- */
49
-
50
- export interface GridColumn {
51
- id: any;
52
- title: string | UnsafeHtml;
53
- width: number;
54
- fixed?: boolean;
55
- flex?: number;
56
- align?: 'left' | 'center' | 'right';
57
- header_align?: 'left' | 'center' | 'right';
58
- renderer?: CellRenderer; // for "renderer" type
59
- formatter?: (input: any) => string; // for "custom" type
60
- type?: ColType;
61
- cls?: string;
62
- sortable?: boolean | SortCallback;
63
- footer_val?: string;
64
- classifier?: CellClassifier;
65
- }
66
-
67
- interface GridColumnEx extends GridColumn {
68
- sens?: "up" | "dn";
69
- }
70
-
71
- export interface GridviewEvents extends ComponentEvents {
72
- click?: EvClick;
73
- dblClick?: EvDblClick;
74
- contextMenu?: EvContextMenu;
75
- selectionChange?: EvSelectionChange;
76
- }
77
-
78
- export interface GridviewProps extends ComponentProps {
79
- footer?: boolean;
80
- store: DataStore;
81
- columns: GridColumn[];
82
- emptyMsg?: string;
83
-
84
- click?: EventCallback<EvClick>;
85
- dblClick?: EventCallback<EvDblClick>;
86
- contextMenu?: EventCallback<EvContextMenu>;
87
- selectionChange?: EventCallback<EvSelectionChange>;
88
- sort?: {
89
- field_id: string;
90
- asc?: boolean;
91
- }
92
- }
93
-
94
- /**
95
- * we can handle
96
- * 4_095 cols and (1_048_575-1)/2 rows (this is a chrome limitation max pixels of scrollbars )
97
- */
98
-
99
- /**
100
- *
101
- */
102
-
103
- @class_ns("x4")
104
- export class Gridview<P extends GridviewProps = GridviewProps, E extends GridviewEvents = GridviewEvents> extends Component<P,E> {
105
-
106
- private _dataview: DataView;
107
- private _datamodel: DataModel;
108
-
109
- private _columns: GridColumnEx[];
110
-
111
- private _lock: number;
112
- private _dirty: number;
113
-
114
- private _row_height: number;
115
-
116
- private _left: number;
117
- private _top: number;
118
-
119
- private _body: Component;
120
- private _viewport: Component;
121
-
122
- private _fheader: Box; // fixed col header
123
- private _hheader: Box; // col header
124
- private _vheader: Box; // vertical row header
125
- private _ffooter: Box; // fixed footer
126
- private _footer: Box; // footer
127
-
128
- private _vis_rows: Map<number, { h: Component, r: Component }>;
129
- private _start: number;
130
- private _end: number;
131
-
132
- private _selection: Set<number>;
133
-
134
-
135
- // TODO: that
136
- private _num_fmt = new Intl.NumberFormat('fr-FR');
137
- private _mny_fmt = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
138
- private _dte_fmt = new Intl.DateTimeFormat('fr-FR', {});
139
- private _dtetme_fmt = new Intl.DateTimeFormat('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:"2-digit"});
140
-
141
- private _has_fixed: boolean;
142
- private _has_footer: boolean;
143
-
144
- constructor(props: P) {
145
- super(props);
146
-
147
- this._lock = 0;
148
- this._dirty = 0;
149
-
150
- this._row_height = 32;
151
-
152
- this._left = 0;
153
- this._top = 0;
154
-
155
- this._vis_rows = new Map();
156
- this._selection = new Set();
157
- this._has_fixed = false;
158
- this._has_footer = props.footer;
159
-
160
- this._columns = props.columns.map(x => x);
161
-
162
- this.mapPropEvents( props, "click", "dblClick", "contextMenu", "selectionChange" );
163
-
164
- this.lock(true);
165
- this.setAttribute("tabindex", 0);
166
-
167
- this.addDOMEvent("created", () => {
168
- this._init();
169
- this._dirty = 1;
170
- this.lock(false);
171
- });
172
-
173
- this.addDOMEvent("resized", () => {
174
- this._updateFlexs( );
175
- this._computeFullSize( );
176
- this._update( true );
177
- });
178
-
179
- this.addDOMEvent( "keydown", (e) => {
180
- this._on_key( e );
181
- })
182
-
183
- if (props.store) {
184
- this.setStore(props.store);
185
-
186
- if( props.sort ) {
187
- this.sortCol( props.sort.field_id, props.sort.asc===true );
188
- }
189
- }
190
- }
191
-
192
- /**
193
- *
194
- */
195
-
196
- private _on_key( ev: KeyboardEvent ) {
197
- if( this.isDisabled() ) {
198
- return;
199
- }
200
-
201
- switch( ev.key ) {
202
- case "ArrowDown": {
203
- this.navigate( kbNav.next );
204
- break;
205
- }
206
-
207
- case "ArrowUp": {
208
- this.navigate( kbNav.prev );
209
- break;
210
- }
211
-
212
- case "Home": {
213
- this.navigate( kbNav.first );
214
- break;
215
- }
216
-
217
- case "End": {
218
- this.navigate( kbNav.last );
219
- break;
220
- }
221
-
222
- case "PageDown": {
223
- this.navigate( kbNav.pgdn );
224
- break;
225
- }
226
-
227
- case "PageUp": {
228
- this.navigate( kbNav.pgup );
229
- break;
230
- }
231
-
232
- default:
233
- return;
234
- }
235
-
236
- ev.preventDefault( );
237
- ev.stopPropagation( );
238
- }
239
-
240
- /**
241
- *
242
- */
243
-
244
- navigate( sens: kbNav ) {
245
- if( !this._selection.size ) {
246
- if( sens==kbNav.next || sens==kbNav.pgdn ) {
247
- sens = kbNav.first;
248
- }
249
- else {
250
- sens = kbNav.last;
251
- }
252
- }
253
-
254
- if( sens==kbNav.first || sens==kbNav.last ) {
255
- let nel = sens==kbNav.first ? 0 : this._dataview.getCount()-1;
256
- this._clearSelection( false );
257
- this._addSelection( nel );
258
- this._scrollToIndex( nel );
259
- return true;
260
- }
261
- else if( sens==kbNav.prev || sens==kbNav.next ) {
262
- const fsel = this._selection.values().next().value;
263
- let nel = sens==kbNav.next ? fsel+1 : fsel-1;
264
- if( nel>=0 && nel<this._dataview.getCount() ) {
265
- this._clearSelection( false );
266
- this._addSelection( nel );
267
- this._scrollToIndex( nel );
268
- return true;
269
- }
270
- }
271
- else if( sens==kbNav.pgdn || sens==kbNav.pgup ) {
272
- const pgh = this._vis_rows.size;
273
-
274
- const fsel = this._selection.values().next().value;
275
-
276
- let sby = sens==kbNav.pgdn ? pgh : -pgh;
277
- let nel = fsel+sby;
278
-
279
- if( nel<0 ) {
280
- nel = 0;
281
- }
282
- else if( nel>=this._dataview.getCount() ) {
283
- nel = this._dataview.getCount()-1;
284
- }
285
-
286
- if( nel!=fsel ) {
287
- this._clearSelection( false );
288
- this._addSelection( nel );
289
-
290
- if (this._dataview.getCount() < SCROLL_LIMIT) {
291
- sby *= this._row_height;
292
- }
293
-
294
- this._viewport.dom.scrollBy( 0, sby );
295
-
296
- return true;
297
- }
298
- }
299
-
300
- return false;
301
- }
302
-
303
- /**
304
- *
305
- */
306
-
307
- private _scrollToIndex( index: number, block = 'nearest' ) {
308
-
309
- // is it already visible ?
310
- let rows = this.queryAll(`.row[data-row="${index}"]`);
311
- if (rows.length) {
312
- rows.forEach( row => {
313
- row.scrollIntoView({ block: block as any } );
314
- } );
315
- }
316
- // nope, refill
317
- else {
318
- let top = index;
319
- if (this._dataview.getCount() < SCROLL_LIMIT) {
320
- top *= this._row_height;
321
- }
322
-
323
- this._viewport.dom.scrollTo( 0, top );
324
- }
325
- }
326
-
327
- /**
328
- *
329
- */
330
-
331
- setStore(store: DataStore) {
332
-
333
- const on_change = (ev: EvViewChange) => {
334
- if( !this._viewport ) {
335
- // not created
336
- return;
337
- }
338
-
339
- if (ev.change_type == 'change') {
340
- this._selection.clear();
341
- }
342
-
343
- this._updateFlexs( );
344
- this._computeFullSize();
345
- this._update(true);
346
- }
347
-
348
- // unlink previous observer
349
- if (this._dataview) {
350
- this._dataview.off('view_change', on_change);
351
- }
352
-
353
- if (store) {
354
- this._dataview = new DataView({ store: store });
355
- this._datamodel = store.getModel();
356
- this._dataview.on('view_change', on_change);
357
- }
358
- else {
359
- this._dataview = null;
360
- this._datamodel = null;
361
- }
362
- }
363
-
364
- setColumns( columns: GridColumn[] ) {
365
- this._columns = columns.map(x => x);
366
- if( this.dom ) {
367
-
368
- this._updateFlexs( );
369
-
370
- // Rebuild headers
371
- if (this._fheader) {
372
- /*
373
- const newFixedHeader = this._buildColHeader(true);
374
- this._fheader.setContent(newFixedHeader.getChildren());
375
- // On doit remplacer _fheader dans le DOM ou mettre à jour son contenu
376
- // La méthode _buildColHeader retourne une Box.
377
- // Ici, je vais simplifier en vidant et remplissant si possible,
378
- // mais Box n'a pas forcément de méthode simple pour remplacer tout le DOM interne sans casser les events.
379
- // Le plus simple est de remplacer les composants header dans le DOM global du gridview.
380
-
381
- // Approche : on recrée les headers et on remplace les anciens
382
- this._fheader.destroy();
383
- this._hheader.destroy();
384
-
385
- this._fheader = this._buildColHeader(true);
386
- this._hheader = this._buildColHeader(false);
387
-
388
- // Il faut réinsérer ces headers au bon endroit dans le DOM du Gridview.
389
- // _init fait:
390
- // this.setContent([
391
- // this._fheader,
392
- // this._hheader,
393
- // this._vheader,
394
- // this._viewport,
395
- // this._ffooter,
396
- // this._footer,
397
- // ]);
398
-
399
- // Donc on peut reconstruire le content complet
400
- const content = [
401
- this._fheader,
402
- this._hheader,
403
- this._vheader,
404
- this._viewport
405
- ];
406
-
407
- if (this._has_footer) {
408
- this._ffooter.destroy();
409
- this._footer.destroy();
410
- this._ffooter = this._buildColFooter(true);
411
- this._footer = this._buildColFooter(false);
412
- content.push(this._ffooter);
413
- content.push(this._footer);
414
- }
415
-
416
- this.setContent(content);
417
- */
418
- console.assert( false, "TODO" );
419
- }
420
-
421
- this._computeFullSize( );
422
- this._update( true );
423
- }
424
- }
425
-
426
- getView( ): DataView {
427
- return this._dataview;
428
- }
429
-
430
- /**
431
- *
432
- */
433
-
434
- lock(lock: boolean) {
435
- if (lock) {
436
- this._lock++;
437
- }
438
- else {
439
- if (--this._lock == 0 && this._dirty) {
440
- this._update( true );
441
- }
442
- }
443
- }
444
-
445
- private _getColCount() {
446
- return this._columns.length;
447
- }
448
-
449
- private _getCol(index: number) {
450
- return this._columns[index];
451
- }
452
-
453
- /**
454
- *
455
- */
456
-
457
- private _buildColHeader(fixed: boolean) {
458
- // row header
459
- const els: Component[] = [];
460
-
461
- const count = this._getColCount();
462
- for (let col = 0; col < count; col++) {
463
- const cdata = this._getCol(col);
464
- if ((!!cdata.fixed) != fixed) {
465
- continue;
466
- }
467
-
468
- const sizer = new CSizer("right");
469
-
470
- sizer.on("stop", ( ) => {
471
- this._updateFlexs( );
472
- })
473
-
474
- sizer.on("resize", (ev) => {
475
- cdata.width = ev.size;
476
- cdata.flex = 0;
477
-
478
- const cols = this.queryAll(`[data-col="${col}"]`)
479
- cols.forEach(c => {
480
- c.setStyleValue("width", ev.size + "px");
481
- });
482
-
483
- const rh = header.getBoundingRect();
484
-
485
- if (!fixed) {
486
- this._body.setStyleValue("width", rh.width + "px");
487
- }
488
- else {
489
- this.setStyleVariable("--fixed-width", rh.width + "px");
490
- }
491
- })
492
-
493
- const cell = new Component({
494
- cls: `cell`,
495
- attrs: { "data-col": col },
496
- style: { width: cdata.width ? cdata.width + "px" : undefined },
497
- content: [
498
- new SimpleText({ cls: 'title', text: cdata.title, align: cdata.header_align ?? "left" }),
499
- new Component({ cls: "sorter" }),
500
- sizer
501
- ]
502
- });
503
-
504
- cell.addDOMEvent("touchend", () => {
505
- const last = cell.getInternalData("touchend");
506
- const now = Date.now();
507
- const delta = last ? now - last : 0;
508
- if (delta > 30 && delta < 300) {
509
- this._sortCol(col);
510
- }
511
- else {
512
- cell.setInternalData("touchend", now);
513
- }
514
- })
515
-
516
- cell.addDOMEvent("dblclick", () => {
517
- this._sortCol(col);
518
- });
519
-
520
- els.push(cell);
521
- }
522
-
523
- if (fixed && els.length == 0) {
524
- return null;
525
- }
526
-
527
- const header = new Box({ cls: "col-header", content: els });
528
- header.setClass("fixed", fixed);
529
-
530
- return header;
531
- }
532
-
533
- /**
534
- *
535
- */
536
-
537
- private _buildColFooter(fixed: boolean) {
538
- // row header
539
- const els: Component[] = [];
540
-
541
- const count = this._getColCount();
542
- for (let col = 0; col < count; col++) {
543
- const cdata = this._getCol(col);
544
- if ((!!cdata.fixed) != fixed) {
545
- continue;
546
- }
547
-
548
- const cell = new Component({
549
- cls: `cell`,
550
- attrs: { "data-col": col },
551
- style: { width: cdata.width ? cdata.width + "px" : undefined },
552
- content: [
553
- new SimpleText({ text: cdata.footer_val }),
554
- ]
555
- });
556
-
557
- cell.addDOMEvent("dblclick", () => {
558
- this._sortCol(col);
559
- });
560
-
561
- els.push(cell);
562
- }
563
-
564
- if (fixed && els.length == 0) {
565
- return null;
566
- }
567
-
568
- const header = new Box({ cls: "col-footer", content: els });
569
- header.setClass("fixed", fixed);
570
-
571
- return header;
572
- }
573
-
574
- /**
575
- *
576
- */
577
-
578
- private _sortCol(col: number, ascending?: boolean ) {
579
-
580
- // to allow cursor
581
- this.setTimeout("sort", 50, () => {
582
- let asc = true;
583
-
584
- // already sorted ?
585
- const scol = this.query(`.col-header .cell[data-col="${col}"]`);
586
- if( !scol ) {
587
- return;
588
- }
589
-
590
- if( ascending===undefined ) {
591
- if (scol.hasClass("sorted")) {
592
- if (scol.hasClass("desc")) {
593
- asc = true;
594
- }
595
- else {
596
- asc = false;
597
- }
598
- }
599
- else {
600
- const sorted = this.queryAll(".sorted");
601
- sorted.forEach(x => x.removeClass("sorted asc desc"));
602
- }
603
- }
604
- else {
605
- asc = ascending;
606
- }
607
-
608
- scol.setClass("sorted");
609
- scol.setClass("desc", !asc);
610
-
611
- const cdata = this._getCol(col);
612
-
613
- let num = false;
614
- switch (cdata.type) {
615
- case "checkbox":
616
- case "money":
617
- case "number":
618
- case "percent": {
619
- num = true;
620
- }
621
- }
622
-
623
- setWaitCursor(true);
624
-
625
- this._dataview.sort([{
626
- field: cdata.id,
627
- ascending: asc,
628
- numeric: num,
629
- callback: cdata.sortable instanceof Function ? cdata.sortable : undefined,
630
- }]);
631
-
632
- this._update(true);
633
-
634
- setWaitCursor(false);
635
- });
636
- }
637
-
638
- /**
639
- *
640
- */
641
-
642
- sortCol( colIdx: any, ascending: boolean ) {
643
-
644
- const idx = this._columns.findIndex( x => x.id === colIdx );
645
- if( idx>=0 ) {
646
- this._sortCol( idx, ascending );
647
- }
648
- }
649
-
650
- /**
651
- *
652
- */
653
-
654
- private _renderCell(rec: DataRecord, column: GridColumnEx, extra_cls: string[] ): ComponentContent {
655
-
656
- const col = column.id;
657
- const type = column.type;
658
-
659
- let data = this._datamodel.getRaw(col, rec);
660
- if (data === undefined || data === null) {
661
- return null;
662
- }
663
-
664
- let cls = "";
665
- if( column.classifier ) {
666
- extra_cls.push( column.classifier( data, rec, col ) );
667
- }
668
-
669
- if (data instanceof Function) {
670
- return data(rec, col);
671
- }
672
-
673
- if( column.formatter ) {
674
- return column.formatter( data );
675
- }
676
-
677
- switch (type) {
678
- case "checkbox": {
679
- if (data) {
680
- return new Icon({ cls: "cell-check" + cls, iconId: check_icon });
681
- }
682
-
683
- return undefined;
684
- }
685
-
686
- case "image": {
687
- if (isString(data)) {
688
- return new Image({ cls, src: data, fit: "scale-down" });
689
- }
690
-
691
- return undefined;
692
- }
693
-
694
- case "number": {
695
- if (!isNumber(data)) {
696
- return "NaN";
697
- }
698
-
699
- data = this._num_fmt.format(data as number);
700
- break;
701
- }
702
-
703
- case "money": {
704
- if (!isNumber(data)) {
705
- return "NaN";
706
- }
707
-
708
- data = this._mny_fmt.format(data as number);
709
- break;
710
- }
711
-
712
- case "percent": {
713
- return new Box({
714
- cls: "percent" + cls,
715
- content: new Component({ cls: "bar", width: data + "%" })
716
- });
717
- }
718
-
719
- case "icon": {
720
- return new Icon({ cls, iconId: data + "" });
721
- }
722
-
723
- case "date": {
724
- if( isString(data) ) {
725
- data = new Date( data );
726
- }
727
-
728
- data = this._dte_fmt.format(data as Date);
729
- break;
730
- }
731
-
732
- case "date-time": {
733
- if( isString(data) ) {
734
- data = new Date( data );
735
- }
736
-
737
- data = this._dtetme_fmt.format(data as Date);
738
- break;
739
- }
740
-
741
- default: {
742
- data = data + "";
743
- break;
744
- }
745
- }
746
-
747
- return new Component({ tag: "span", cls, content: data });
748
- }
749
-
750
- /**
751
- *
752
- */
753
-
754
- private _buildRow(rowid: number, rec: DataRecord, top: number) {
755
-
756
- const els: Component[] = [];
757
- const count = this._getColCount();
758
-
759
- for (let col = 0; col < count; col++) {
760
- const cdata = this._getCol(col);
761
- if (cdata.fixed) {
762
- continue;
763
- }
764
-
765
- const extra: string[] = []
766
- const content = this._renderCell(rec, cdata, extra );
767
-
768
- let align = "start";
769
- switch (cdata.align) {
770
- default: align = "start"; break;
771
- case "center": align = "center"; break;
772
- case "right": align = "end"; break;
773
- }
774
-
775
- const el = new Component({
776
- cls: "cell",
777
- style: { width: cdata?.width ? cdata.width + "px" : undefined, justifyContent: align },
778
- content
779
- });
780
-
781
- if( extra.length ) {
782
- el.addClass( extra.join(' ') );
783
- }
784
-
785
- if (cdata.type) {
786
- el.addClass(cdata.type);
787
- }
788
-
789
- el.setData("col", col + "");
790
- el.setData("row", rowid + "")
791
-
792
- els.push(el);
793
- }
794
-
795
- const rowel = new Box({ cls: "row", style: { top: top.toFixed(2) + "px" }, content: els });
796
- rowel.setData("row", rowid + "");
797
-
798
- if (this._selection.has(rowid)) {
799
- rowel.addClass("selected");
800
- }
801
-
802
- if( rowid&1 ) {
803
- rowel.addClass( "even" );
804
- }
805
- else {
806
- rowel.addClass( "odd" );
807
- }
808
-
809
- return rowel;
810
- }
811
-
812
- /**
813
- *
814
- */
815
-
816
- private _buildRowHeader(rowid: number, rec: DataRecord, top: number) {
817
-
818
- const cols: Component[] = [];
819
- const count = this._getColCount();
820
-
821
- for (let col = 0; col < count; col++) {
822
- const cdata = this._getCol(col);
823
- if (!cdata?.fixed) {
824
- continue;
825
- }
826
-
827
- const content = this._renderCell(rec, cdata.id, [cdata.type] );
828
-
829
- let align = "start";
830
- switch (cdata.align) {
831
- default: align = "start"; break;
832
- case "center": align = "center"; break;
833
- case "right": align = "end"; break;
834
- }
835
-
836
- const el = new Component({
837
- cls: "cell",
838
- style: { width: cdata?.width ? cdata.width + "px" : undefined, justifyContent: align },
839
- content
840
- });
841
-
842
- if (cdata.type) {
843
- el.addClass(cdata.type);
844
- }
845
-
846
- el.setData("col", col + "");
847
- el.setData("row", rowid + "")
848
-
849
- cols.push(el);
850
- }
851
-
852
- const rowel = new Box({ cls: "row", style: { top: top + "px" }, content: cols });
853
- rowel.setData("row", rowid + "");
854
-
855
- if (this._selection.has(rowid)) {
856
- rowel.addClass("selected");
857
- }
858
-
859
- return rowel;
860
- }
861
-
862
- /**
863
- *
864
- */
865
-
866
- private _updateFlexs( ) {
867
- let maxw = 0;
868
- let flexc = 0;
869
-
870
- const ccount = this._getColCount();
871
-
872
- for (let x = 0; x < ccount; x++) {
873
- const cdata = this._getCol(x);
874
-
875
- if( !cdata.fixed && cdata.flex ) {
876
- flexc += cdata.flex;
877
- }
878
- else {
879
- maxw += cdata.width;
880
- }
881
- }
882
-
883
- if( flexc ) {
884
- const width = this._viewport.dom.clientWidth;
885
- const delta = width - maxw;
886
- const fw = delta / flexc;
887
-
888
- for (let col = 0; col < ccount; col++) {
889
- const cdata = this._getCol(col);
890
- if( !cdata.fixed && cdata.flex ) {
891
- cdata.width = Math.max( cdata.flex*fw, 32 );
892
-
893
- const cols = this.queryAll(`[data-col="${col}"]`)
894
- cols.forEach(c => {
895
- c.setStyleValue("width", cdata.width + "px");
896
- });
897
- }
898
- }
899
- }
900
- }
901
-
902
- /**
903
- *
904
- */
905
-
906
- private _computeFullSize() {
907
-
908
- let maxw = 0;
909
- let maxfw = 0;
910
-
911
- const ccount = this._getColCount();
912
-
913
- for (let x = 0; x < ccount; x++) {
914
- const cdata = this._getCol(x);
915
- let w = 0;
916
-
917
- if (cdata.fixed) {
918
- this._has_fixed = true;
919
- }
920
-
921
- if ( cdata.width) {
922
- w += cdata.width;
923
- }
924
-
925
- if (cdata.fixed) {
926
- maxfw += w;
927
- }
928
- else {
929
- maxw += w;
930
- }
931
- }
932
-
933
- const maxr = this._dataview ? this._dataview.getCount() : 0;
934
- let maxh = maxr;
935
-
936
- if (maxr < SCROLL_LIMIT) {
937
- maxh *= this._row_height;
938
- }
939
- else {
940
- const height = this._body.dom.parentElement.clientHeight;
941
- const npage = height / this._row_height;
942
- maxh = maxr - Math.floor(npage) + npage * this._row_height;
943
- }
944
-
945
- this.setStyleVariable("--fixed-width", maxfw + "px");
946
- this._body.setStyleValue("height", maxh==0 ? "100%" : maxh + "px");
947
- this._body.setStyleValue("width", maxw + "px");
948
- this._vheader.setStyleValue("height", maxh + "px");
949
- }
950
-
951
- /**
952
- *
953
- */
954
-
955
- private _init() {
956
- this._body = new Component({ cls: "body" });
957
-
958
- this._viewport = new Viewport({ content: this._body });
959
-
960
- if (!this._has_footer) {
961
- this.setStyleVariable("--footer-height", "0");
962
- }
963
-
964
- // SCROLL
965
- this._viewport.addDOMEvent( "scroll", (ev) => {
966
- // sync horz & vert elements
967
- this._left = this._viewport.dom.scrollLeft;
968
- this.setStyleVariable("--left", -this._left + "px");
969
-
970
- this._top = this._viewport.dom.scrollTop;
971
- this.setStyleVariable("--top", -this._top + "px");
972
-
973
- //this.setTimeout( "update", 0, ( ) => this._update( ) );
974
- this._update()
975
- });
976
-
977
- // WHEEL
978
- this.addDOMEvent("wheel", (ev: WheelEvent) => {
979
- if (ev.deltaY && this._dataview && this._dataview.getCount() >= SCROLL_LIMIT) {
980
- this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -1 : 1);
981
- ev.stopPropagation();
982
- ev.preventDefault();
983
- }
984
-
985
- if (this._has_fixed && ev.deltaY) {
986
- // wheel on fixed part
987
- // fixed part do not have scrollbar, so we need to handle it by hand
988
- let t = ev.target as Node;
989
- while (t != this.dom) {
990
- if (t == this._vheader.dom) {
991
- this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -this._row_height : this._row_height);
992
- ev.stopPropagation();
993
- ev.preventDefault();
994
- break;
995
- }
996
-
997
- t = t.parentNode;
998
- }
999
- }
1000
- })
1001
-
1002
- const targetRow = ( e: MouseEvent ) => {
1003
- let el = Component.parentElement(e.target as HTMLElement, Component);
1004
- while (el && !el.hasClass("row")) {
1005
- el = el.parentElement();
1006
- }
1007
-
1008
- if (el) {
1009
- return el.getIntData("row");
1010
- }
1011
-
1012
- return undefined;
1013
- }
1014
-
1015
-
1016
- // CLICK
1017
- this.addDOMEvent("click", (e) => {
1018
- const row = targetRow( e );
1019
- if (row!==undefined ) {
1020
- //TODO: multiselection
1021
- if( !this._selection.has(row) ) {
1022
- this._clearSelection( false );
1023
- this._addSelection(row);
1024
- }
1025
- }
1026
- else {
1027
- this._clearSelection( true );
1028
- }
1029
- });
1030
-
1031
- // DBLCLICK
1032
- this.addDOMEvent("dblclick", (e) => {
1033
- const row = targetRow( e );
1034
- if (row!==undefined ) {
1035
- //TODO: multiselection
1036
- if( !this._selection.has(row) ) {
1037
- this._clearSelection( false );
1038
- this._addSelection(row);
1039
- }
1040
-
1041
- this._on_dblclk( e, row );
1042
-
1043
- const rec = this._dataview.getByIndex( row );
1044
- this.fire( "dblClick", { context: rec } );
1045
- }
1046
- });
1047
-
1048
- // CONTEXT
1049
- this.addDOMEvent("contextmenu", (e) => {
1050
- const row = targetRow( e );
1051
- if (row!==undefined ) {
1052
- //TODO: multiselection
1053
- if( !this._selection.has(row) ) {
1054
- this._clearSelection( false );
1055
- this._addSelection(row);
1056
- }
1057
-
1058
- const rec = this._dataview.getByIndex( row );
1059
- this.fire( "contextMenu", { uievent: e, context: rec } );
1060
- }
1061
-
1062
- e.preventDefault( );
1063
- e.stopPropagation( );
1064
- });
1065
-
1066
- // MOUSE OVER
1067
- this.addDOMEvent("mouseover", (e) => {
1068
-
1069
- if (!this._has_fixed) {
1070
- return;
1071
- }
1072
-
1073
- let el = Component.parentElement(e.target as HTMLElement, Component);
1074
- while (el && !el.hasClass("row")) {
1075
- el = el.parentElement();
1076
- }
1077
-
1078
- if (el) {
1079
- const data = el.getData("row");
1080
-
1081
- this.queryAll(".hover").forEach(x => x.removeClass("hover"));
1082
-
1083
- if (data) {
1084
- const rows = this.queryAll(`.row[data-row="${data}"]`);
1085
- rows.forEach(x => x.addClass("hover"));
1086
- }
1087
- }
1088
- });
1089
-
1090
- // MOUSE LEAVE
1091
- this.addDOMEvent("mouseleave", (e) => {
1092
-
1093
- if (!this._has_fixed) {
1094
- return;
1095
- }
1096
-
1097
- this.queryAll(".hover").forEach(x => x.removeClass("hover"));
1098
- });
1099
-
1100
- this._updateFlexs( );
1101
-
1102
- this._fheader = this._buildColHeader(true);
1103
- this._hheader = this._buildColHeader(false);
1104
- this._vheader = new Box({ cls: "row-header" })
1105
-
1106
- if (this._has_footer) {
1107
- this._ffooter = this._buildColFooter(true);
1108
- this._footer = this._buildColFooter(false);
1109
- }
1110
-
1111
- this.setContent([this._viewport, this._fheader, this._hheader, this._ffooter, this._footer, this._vheader]);
1112
-
1113
- // compute misc variables
1114
- {
1115
- const rh = this.getStyleVariable("--row-height");
1116
- this._row_height = parseInt(rh);
1117
- }
1118
-
1119
- this._computeFullSize();
1120
- }
1121
-
1122
- /**
1123
- *
1124
- */
1125
-
1126
- protected _on_dblclk( e: UIEvent, row: number ) {
1127
-
1128
- }
1129
-
1130
- /**
1131
- *
1132
- */
1133
-
1134
- private _update(force = false) {
1135
-
1136
- if (!this._lock) {
1137
- const rc = this.getBoundingRect();
1138
-
1139
- // rows
1140
- const rowc = this._dataview ? this._dataview.getCount() : 0;
1141
-
1142
- // empty ?
1143
- if( rowc==0 ) {
1144
- this._body.setContent( new Label( { cls: "empty vertical", icon: empty_icon, text: this.props.emptyMsg ?? _tr.global.empty_list } ) );
1145
- return;
1146
- }
1147
-
1148
-
1149
- const mul = rowc < SCROLL_LIMIT ? this._row_height : 1;
1150
-
1151
- const start = Math.floor(this._top / mul);
1152
- const end = start + Math.ceil(rc.height / this._row_height);
1153
- const hasFixed = this._has_fixed;
1154
-
1155
- if (this._start != start || this._end != end || force) {
1156
-
1157
- const rows: Component[] = [];
1158
- const headers: Component[] = [];
1159
-
1160
- if (force) {
1161
- this._vis_rows.clear();
1162
- }
1163
-
1164
- let newvis: typeof this._vis_rows = new Map();
1165
-
1166
- let y = start * mul;
1167
-
1168
- for (let row = start; row < end && row < rowc; row++, y += this._row_height) {
1169
-
1170
- let el = this._vis_rows.get(row);
1171
- const rec = this._dataview.getByIndex(row);
1172
-
1173
- if (hasFixed) {
1174
- if (!el) {
1175
- el = {
1176
- h: this._buildRowHeader(row, rec, y),
1177
- r: this._buildRow(row, rec, y),
1178
- };
1179
- }
1180
- else {
1181
- el.h.setStyleValue("top", y + "px");
1182
- el.r.setStyleValue("top", y + "px");
1183
- }
1184
-
1185
- headers.push(el.h);
1186
- }
1187
- else {
1188
- if (!el) {
1189
- el = { h: null, r: this._buildRow(row, rec, y), };
1190
- }
1191
- else {
1192
- el.r.setStyleValue("top", y + "px");
1193
- }
1194
- }
1195
-
1196
- rows.push(el.r);
1197
- newvis.set(row, el);
1198
- }
1199
-
1200
- if (hasFixed) {
1201
- headers.push(new Component({ cls: "cell-out", style: { top: y + "px" } }));
1202
- }
1203
-
1204
- this._vis_rows = newvis;
1205
- this._start = start;
1206
- this._end = end;
1207
-
1208
- this._body.setContent(rows);
1209
-
1210
- if (hasFixed) {
1211
- this._vheader.removeClass("@hidden");
1212
- this._vheader.setContent(headers);
1213
- }
1214
- else {
1215
- this._vheader.addClass("@hidden");
1216
- }
1217
- }
1218
- }
1219
- }
1220
- /**
1221
- *
1222
- */
1223
-
1224
- clearSelection( ) {
1225
- this._clearSelection( false );
1226
- }
1227
-
1228
- private _clearSelection( notify: boolean ) {
1229
- if( !this._selection.size ) {
1230
- return;
1231
- }
1232
-
1233
- for (const ref of this._selection.keys()) {
1234
- const els = this.queryAll(`.row[data-row="${ref}"]`)
1235
- els.forEach(el => {
1236
- el.removeClass("selected");
1237
- })
1238
- }
1239
-
1240
- this._selection.clear();
1241
- if( notify ) {
1242
- this.fire("selectionChange", { selection: [], empty: true } );
1243
- }
1244
- }
1245
-
1246
- /**
1247
- *
1248
- */
1249
-
1250
- private _addSelection(rowid: number) {
1251
- this._selection.add(rowid)
1252
-
1253
- const els = this.queryAll(`.row[data-row="${rowid}"]`)
1254
- els.forEach(el => {
1255
- el.addClass("selected");
1256
- });
1257
-
1258
- const rec = this._dataview.getByIndex( rowid );
1259
- this.fire("selectionChange", { selection: [rec], empty: false } );
1260
- }
1261
-
1262
- /**
1263
- *
1264
- */
1265
-
1266
- getSelection( ) {
1267
- if( this._selection.size==0 ) {
1268
- return null;
1269
- }
1270
-
1271
- const ids = [...this._selection.values() ];
1272
- return ids.map( id => this._dataview.getByIndex(id) );
1273
- }
1274
-
1275
- /**
1276
- *
1277
- */
1278
-
1279
- getFirstSel( ) {
1280
- if( this._selection.size==0 ) {
1281
- return null;
1282
- }
1283
-
1284
- const id = this._selection.values().next().value;
1285
- return this._dataview.getByIndex( id );
1286
- }
1287
-
1288
- /**
1289
- *
1290
- */
1291
-
1292
- selectItem( id: any, ensureVisible = true ) {
1293
- const index = this._dataview.indexOfId( id );
1294
- if( index>=0 ) {
1295
- this._addSelection( index );
1296
- if( ensureVisible ) {
1297
- this._scrollToIndex( index );
1298
- }
1299
- }
1300
- }
1301
-
1302
- /**
1303
- *
1304
- */
1305
-
1306
- setColTitle( col_name: any, title: string ) {
1307
- const col = this._columns.findIndex( x => x.id==col_name );
1308
- if( col>=0 ) {
1309
- this._columns[col].title = title;
1310
- const el = this._hheader.query<SimpleText>(`[data-col="${col}"] .title` )
1311
- el.setText( title );
1312
- }
1313
- }
1314
- }
1315
-
1316
-
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file gridview.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 { Component, ComponentContent, ComponentEvents, ComponentProps, EvClick, EvContextMenu, EvDblClick, EvSelectionChange, componentFromDOM } from '../../core/component';
19
+ import { class_ns, isNumber, isString, setWaitCursor, UnsafeHtml } from '../../core/core_tools';
20
+ import { DataModel, DataStore, DataView, DataRecord, EvViewChange } from '../../core/core_data';
21
+ import { EventCallback } from '../../core/core_events';
22
+ import { kbNav } from '../../core/core_tools';
23
+
24
+ import { Icon } from '../icon/icon';
25
+ import { Image } from '../image/image'
26
+ import { Box } from '../boxes/boxes';
27
+ import { CSizer } from '../sizers/sizer'
28
+ import { Viewport } from '../viewport/viewport';
29
+ import { SimpleText } from '../label/label';
30
+
31
+ import check_icon from "../checkbox/check.svg";
32
+ import "./gridview.module.scss"
33
+
34
+ export type CellRenderer = (rec: DataRecord) => Component;
35
+ export type CellClassifier = (data: any, rec: DataRecord, col: string ) => string; // return the cell computed class
36
+
37
+ type ColType = "number" | "money" | "checkbox" | "date" | "string" | "image" | "percent" | "icon" | "date-time";
38
+
39
+ const SCROLL_LIMIT = 200;
40
+
41
+
42
+ /**
43
+ *
44
+ */
45
+
46
+ export interface GridColumn {
47
+ id: any;
48
+ title: string | UnsafeHtml;
49
+ width: number;
50
+ fixed?: boolean;
51
+ flex?: number;
52
+ align?: 'left' | 'center' | 'right';
53
+ header_align?: 'left' | 'center' | 'right';
54
+ renderer?: CellRenderer; // for "renderer" type
55
+ formatter?: (input: any) => string; // for "custom" type
56
+ type?: ColType;
57
+ cls?: string;
58
+ sortable?: boolean;
59
+ footer_val?: string;
60
+ classifier?: CellClassifier;
61
+ }
62
+
63
+ interface GridColumnEx extends GridColumn {
64
+ sens?: "up" | "dn";
65
+ }
66
+
67
+ export interface GridviewEvents extends ComponentEvents {
68
+ click?: EvClick;
69
+ dblClick?: EvDblClick;
70
+ contextMenu?: EvContextMenu;
71
+ selectionChange?: EvSelectionChange;
72
+ }
73
+
74
+ export interface GridviewProps extends ComponentProps {
75
+ footer?: boolean;
76
+ store: DataStore;
77
+ columns: GridColumn[];
78
+
79
+ click?: EventCallback<EvClick>;
80
+ dblClick?: EventCallback<EvDblClick>;
81
+ contextMenu?: EventCallback<EvContextMenu>;
82
+ selectionChange?: EventCallback<EvSelectionChange>;
83
+ }
84
+
85
+ /**
86
+ * we can handle
87
+ * 4_095 cols and (1_048_575-1)/2 rows (this is a chrome limitation max pixels of scrollbars )
88
+ */
89
+
90
+ /**
91
+ *
92
+ */
93
+
94
+ @class_ns("x4")
95
+ export class Gridview<P extends GridviewProps = GridviewProps, E extends GridviewEvents = GridviewEvents> extends Component<P,E> {
96
+
97
+ private _dataview: DataView;
98
+ private _datamodel: DataModel;
99
+
100
+ private _columns: GridColumnEx[];
101
+
102
+ private _lock: number;
103
+ private _dirty: number;
104
+
105
+ private _row_height: number;
106
+
107
+ private _left: number;
108
+ private _top: number;
109
+
110
+ private _body: Component;
111
+ private _viewport: Component;
112
+
113
+ private _fheader: Box; // fixed col header
114
+ private _hheader: Box; // col header
115
+ private _vheader: Box; // vertical row header
116
+ private _ffooter: Box; // fixed footer
117
+ private _footer: Box; // footer
118
+
119
+ private _vis_rows: Map<number, { h: Component, r: Component }>;
120
+ private _start: number;
121
+ private _end: number;
122
+
123
+ private _selection: Set<number>;
124
+
125
+ // TODO: that
126
+ private _num_fmt = new Intl.NumberFormat('fr-FR');
127
+ private _mny_fmt = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
128
+ private _dte_fmt = new Intl.DateTimeFormat('fr-FR', {});
129
+ private _dtetme_fmt = new Intl.DateTimeFormat('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:"2-digit"});
130
+
131
+ private _has_fixed: boolean;
132
+ private _has_footer: boolean;
133
+
134
+ constructor(props: P) {
135
+ super(props);
136
+
137
+ this._lock = 0;
138
+ this._dirty = 0;
139
+
140
+ this._row_height = 32;
141
+
142
+ this._left = 0;
143
+ this._top = 0;
144
+
145
+ this._vis_rows = new Map();
146
+ this._selection = new Set();
147
+ this._has_fixed = false;
148
+ this._has_footer = props.footer;
149
+
150
+ this._columns = props.columns.map(x => x);
151
+
152
+ this.mapPropEvents( props, "click", "dblClick", "contextMenu", "selectionChange" );
153
+
154
+ this.lock(true);
155
+ this.setAttribute("tabindex", 0);
156
+
157
+ this.addDOMEvent("created", () => {
158
+ this._init();
159
+ this._dirty = 1;
160
+ this.lock(false);
161
+ });
162
+
163
+ this.addDOMEvent("resized", () => {
164
+ this._updateFlexs( );
165
+ this._computeFullSize( );
166
+ this._update( true );
167
+ });
168
+
169
+ this.addDOMEvent( "keydown", (e) => {
170
+ this._on_key( e );
171
+ })
172
+
173
+ if (props.store) {
174
+ this.setStore(props.store);
175
+ }
176
+
177
+ }
178
+
179
+ /**
180
+ *
181
+ */
182
+
183
+ private _on_key( ev: KeyboardEvent ) {
184
+ if( this.isDisabled() ) {
185
+ return;
186
+ }
187
+
188
+ switch( ev.key ) {
189
+ case "ArrowDown": {
190
+ this.navigate( kbNav.next );
191
+ break;
192
+ }
193
+
194
+ case "ArrowUp": {
195
+ this.navigate( kbNav.prev );
196
+ break;
197
+ }
198
+
199
+ case "Home": {
200
+ this.navigate( kbNav.first );
201
+ break;
202
+ }
203
+
204
+ case "End": {
205
+ this.navigate( kbNav.last );
206
+ break;
207
+ }
208
+
209
+ case "PageDown": {
210
+ this.navigate( kbNav.pgdn );
211
+ break;
212
+ }
213
+
214
+ case "PageUp": {
215
+ this.navigate( kbNav.pgup );
216
+ break;
217
+ }
218
+
219
+ default:
220
+ return;
221
+ }
222
+
223
+ ev.preventDefault( );
224
+ ev.stopPropagation( );
225
+ }
226
+
227
+ /**
228
+ *
229
+ */
230
+
231
+ navigate( sens: kbNav ) {
232
+ if( !this._selection.size ) {
233
+ if( sens==kbNav.next || sens==kbNav.pgdn ) {
234
+ sens = kbNav.first;
235
+ }
236
+ else {
237
+ sens = kbNav.last;
238
+ }
239
+ }
240
+
241
+ if( sens==kbNav.first || sens==kbNav.last ) {
242
+ let nel = sens==kbNav.first ? 0 : this._dataview.getCount()-1;
243
+ this._clearSelection();
244
+ this._addSelection( nel );
245
+ this._scrollToIndex( nel );
246
+ return true;
247
+ }
248
+ else if( sens==kbNav.prev || sens==kbNav.next ) {
249
+ const fsel = this._selection.values().next().value;
250
+ let nel = sens==kbNav.next ? fsel+1 : fsel-1;
251
+ if( nel>=0 && nel<this._dataview.getCount() ) {
252
+ this._clearSelection();
253
+ this._addSelection( nel );
254
+ this._scrollToIndex( nel );
255
+ return true;
256
+ }
257
+ }
258
+ else if( sens==kbNav.pgdn || sens==kbNav.pgup ) {
259
+ const pgh = this._vis_rows.size;
260
+
261
+ const fsel = this._selection.values().next().value;
262
+
263
+ let sby = sens==kbNav.pgdn ? pgh : -pgh;
264
+ let nel = fsel+sby;
265
+
266
+ if( nel<0 ) {
267
+ nel = 0;
268
+ }
269
+ else if( nel>=this._dataview.getCount() ) {
270
+ nel = this._dataview.getCount()-1;
271
+ }
272
+
273
+ if( nel!=fsel ) {
274
+ this._clearSelection();
275
+ this._addSelection( nel );
276
+
277
+ if (this._dataview.getCount() < SCROLL_LIMIT) {
278
+ sby *= this._row_height;
279
+ }
280
+
281
+ this._viewport.dom.scrollBy( 0, sby );
282
+
283
+ return true;
284
+ }
285
+ }
286
+
287
+ return false;
288
+ }
289
+
290
+ /**
291
+ *
292
+ */
293
+
294
+ private _scrollToIndex( index: number, block = 'nearest' ) {
295
+
296
+ // is it already visible ?
297
+ let rows = this.queryAll(`.row[data-row="${index}"]`);
298
+ if (rows.length) {
299
+ rows.forEach( row => {
300
+ row.scrollIntoView({ block: block as any } );
301
+ } );
302
+ }
303
+ // nope, refill
304
+ else {
305
+ let top = index;
306
+ if (this._dataview.getCount() < SCROLL_LIMIT) {
307
+ top *= this._row_height;
308
+ }
309
+
310
+ this._viewport.dom.scrollTo( 0, top );
311
+ }
312
+ }
313
+
314
+ /**
315
+ *
316
+ */
317
+
318
+ setStore(store: DataStore) {
319
+
320
+ const on_change = (ev: EvViewChange) => {
321
+ if( !this._viewport ) {
322
+ // not created
323
+ return;
324
+ }
325
+
326
+ if (ev.change_type == 'change') {
327
+ this._selection.clear();
328
+ }
329
+
330
+ this._updateFlexs( );
331
+ this._computeFullSize();
332
+ this._update(true);
333
+ }
334
+
335
+ // unlink previous observer
336
+ if (this._dataview) {
337
+ this._dataview.off('view_change', on_change);
338
+ }
339
+
340
+ if (store) {
341
+ this._dataview = new DataView({ store: store });
342
+ this._datamodel = store.getModel();
343
+ this._dataview.on('view_change', on_change);
344
+ }
345
+ else {
346
+ this._dataview = null;
347
+ this._datamodel = null;
348
+ }
349
+ }
350
+
351
+ setColumns( columns: GridColumn[] ) {
352
+ this._columns = columns.map(x => x);
353
+ if( this.dom ) {
354
+
355
+ this._updateFlexs( );
356
+
357
+ // Rebuild headers
358
+ if (this._fheader) {
359
+ /*
360
+ const newFixedHeader = this._buildColHeader(true);
361
+ this._fheader.setContent(newFixedHeader.getChildren());
362
+ // On doit remplacer _fheader dans le DOM ou mettre à jour son contenu
363
+ // La méthode _buildColHeader retourne une Box.
364
+ // Ici, je vais simplifier en vidant et remplissant si possible,
365
+ // mais Box n'a pas forcément de méthode simple pour remplacer tout le DOM interne sans casser les events.
366
+ // Le plus simple est de remplacer les composants header dans le DOM global du gridview.
367
+
368
+ // Approche : on recrée les headers et on remplace les anciens
369
+ this._fheader.destroy();
370
+ this._hheader.destroy();
371
+
372
+ this._fheader = this._buildColHeader(true);
373
+ this._hheader = this._buildColHeader(false);
374
+
375
+ // Il faut réinsérer ces headers au bon endroit dans le DOM du Gridview.
376
+ // _init fait:
377
+ // this.setContent([
378
+ // this._fheader,
379
+ // this._hheader,
380
+ // this._vheader,
381
+ // this._viewport,
382
+ // this._ffooter,
383
+ // this._footer,
384
+ // ]);
385
+
386
+ // Donc on peut reconstruire le content complet
387
+ const content = [
388
+ this._fheader,
389
+ this._hheader,
390
+ this._vheader,
391
+ this._viewport
392
+ ];
393
+
394
+ if (this._has_footer) {
395
+ this._ffooter.destroy();
396
+ this._footer.destroy();
397
+ this._ffooter = this._buildColFooter(true);
398
+ this._footer = this._buildColFooter(false);
399
+ content.push(this._ffooter);
400
+ content.push(this._footer);
401
+ }
402
+
403
+ this.setContent(content);
404
+ */
405
+ console.assert( false, "TODO" );
406
+ }
407
+
408
+ this._computeFullSize( );
409
+ this._update( true );
410
+ }
411
+ }
412
+
413
+ getView( ): DataView {
414
+ return this._dataview;
415
+ }
416
+
417
+ /**
418
+ *
419
+ */
420
+
421
+ lock(lock: boolean) {
422
+ if (lock) {
423
+ this._lock++;
424
+ }
425
+ else {
426
+ if (--this._lock == 0 && this._dirty) {
427
+ this._update( true );
428
+ }
429
+ }
430
+ }
431
+
432
+ private _getColCount() {
433
+ return this._columns.length;
434
+ }
435
+
436
+ private _getCol(index: number) {
437
+ return this._columns[index];
438
+ }
439
+
440
+ /**
441
+ *
442
+ */
443
+
444
+ private _buildColHeader(fixed: boolean) {
445
+ // row header
446
+ const els: Component[] = [];
447
+
448
+ const count = this._getColCount();
449
+ for (let col = 0; col < count; col++) {
450
+ const cdata = this._getCol(col);
451
+ if ((!!cdata.fixed) != fixed) {
452
+ continue;
453
+ }
454
+
455
+ const sizer = new CSizer("right");
456
+
457
+ sizer.on("stop", ( ) => {
458
+ this._updateFlexs( );
459
+ })
460
+
461
+ sizer.on("resize", (ev) => {
462
+ cdata.width = ev.size;
463
+ cdata.flex = 0;
464
+
465
+ const cols = this.queryAll(`[data-col="${col}"]`)
466
+ cols.forEach(c => {
467
+ c.setStyleValue("width", ev.size + "px");
468
+ });
469
+
470
+ const rh = header.getBoundingRect();
471
+
472
+ if (!fixed) {
473
+ this._body.setStyleValue("width", rh.width + "px");
474
+ }
475
+ else {
476
+ this.setStyleVariable("--fixed-width", rh.width + "px");
477
+ }
478
+ })
479
+
480
+ const cell = new Component({
481
+ cls: `cell`,
482
+ attrs: { "data-col": col },
483
+ style: { width: cdata.width ? cdata.width + "px" : undefined },
484
+ content: [
485
+ new SimpleText({ cls: 'title', text: cdata.title, align: cdata.header_align ?? "left" }),
486
+ new Component({ cls: "sorter" }),
487
+ sizer
488
+ ]
489
+ });
490
+
491
+ cell.addDOMEvent("touchend", () => {
492
+ const last = cell.getInternalData("touchend");
493
+ const now = Date.now();
494
+ const delta = last ? now - last : 0;
495
+ if (delta > 30 && delta < 300) {
496
+ this._sortCol(col);
497
+ }
498
+ else {
499
+ cell.setInternalData("touchend", now);
500
+ }
501
+ })
502
+
503
+ cell.addDOMEvent("dblclick", () => {
504
+ this._sortCol(col);
505
+ });
506
+
507
+ els.push(cell);
508
+ }
509
+
510
+ if (fixed && els.length == 0) {
511
+ return null;
512
+ }
513
+
514
+ const header = new Box({ cls: "col-header", content: els });
515
+ header.setClass("fixed", fixed);
516
+
517
+ return header;
518
+ }
519
+
520
+ /**
521
+ *
522
+ */
523
+
524
+ private _buildColFooter(fixed: boolean) {
525
+ // row header
526
+ const els: Component[] = [];
527
+
528
+ const count = this._getColCount();
529
+ for (let col = 0; col < count; col++) {
530
+ const cdata = this._getCol(col);
531
+ if ((!!cdata.fixed) != fixed) {
532
+ continue;
533
+ }
534
+
535
+ const cell = new Component({
536
+ cls: `cell`,
537
+ attrs: { "data-col": col },
538
+ style: { width: cdata.width ? cdata.width + "px" : undefined },
539
+ content: [
540
+ new SimpleText({ text: cdata.footer_val }),
541
+ ]
542
+ });
543
+
544
+ cell.addDOMEvent("dblclick", () => {
545
+ this._sortCol(col);
546
+ });
547
+
548
+ els.push(cell);
549
+ }
550
+
551
+ if (fixed && els.length == 0) {
552
+ return null;
553
+ }
554
+
555
+ const header = new Box({ cls: "col-footer", content: els });
556
+ header.setClass("fixed", fixed);
557
+
558
+ return header;
559
+ }
560
+
561
+ /**
562
+ *
563
+ */
564
+
565
+ private _sortCol(col: number, ascending?: boolean ) {
566
+
567
+ // to allow cursor
568
+ this.setTimeout("sort", 50, () => {
569
+ let asc = true;
570
+
571
+ // already sorted ?
572
+ const scol = this.query(`.col-header .cell[data-col="${col}"]`);
573
+ if( !scol ) {
574
+ return;
575
+ }
576
+
577
+ if( ascending===undefined ) {
578
+ if (scol.hasClass("sorted")) {
579
+ if (scol.hasClass("desc")) {
580
+ asc = true;
581
+ }
582
+ else {
583
+ asc = false;
584
+ }
585
+ }
586
+ else {
587
+ const sorted = this.queryAll(".sorted");
588
+ sorted.forEach(x => x.removeClass("sorted asc desc"));
589
+ }
590
+ }
591
+ else {
592
+ asc = ascending;
593
+ }
594
+
595
+ scol.setClass("sorted");
596
+ scol.setClass("desc", !asc);
597
+
598
+ const cdata = this._getCol(col);
599
+
600
+ let num = false;
601
+ switch (cdata.type) {
602
+ case "checkbox":
603
+ case "money":
604
+ case "number":
605
+ case "percent": {
606
+ num = true;
607
+ }
608
+ }
609
+
610
+ setWaitCursor(true);
611
+
612
+ this._dataview.sort([{
613
+ field: cdata.id,
614
+ ascending: asc,
615
+ numeric: num
616
+ }]);
617
+
618
+ this._update(true);
619
+
620
+ setWaitCursor(false);
621
+ });
622
+ }
623
+
624
+ /**
625
+ *
626
+ */
627
+
628
+ sortCol( colIdx: any, ascending: boolean ) {
629
+
630
+ const idx = this._columns.findIndex( x => x.id === colIdx );
631
+ if( idx>=0 ) {
632
+ this._sortCol( idx, ascending );
633
+ }
634
+ }
635
+
636
+ /**
637
+ *
638
+ */
639
+
640
+ private _renderCell(rec: DataRecord, column: GridColumnEx, extra_cls: string[] ): ComponentContent {
641
+
642
+ const col = column.id;
643
+ const type = column.type;
644
+
645
+ let data = this._datamodel.getRaw(col, rec);
646
+ if (data === undefined || data === null) {
647
+ return null;
648
+ }
649
+
650
+ let cls = "";
651
+ if( column.classifier ) {
652
+ extra_cls.push( column.classifier( data, rec, col ) );
653
+ }
654
+
655
+ if (data instanceof Function) {
656
+ return data(rec, col);
657
+ }
658
+
659
+ if( column.formatter ) {
660
+ return column.formatter( data );
661
+ }
662
+
663
+ switch (type) {
664
+ case "checkbox": {
665
+ if (data) {
666
+ return new Icon({ cls: "cell-check" + cls, iconId: check_icon });
667
+ }
668
+
669
+ return undefined;
670
+ }
671
+
672
+ case "image": {
673
+ if (isString(data)) {
674
+ return new Image({ cls, src: data, fit: "scale-down" });
675
+ }
676
+
677
+ return undefined;
678
+ }
679
+
680
+ case "number": {
681
+ if (!isNumber(data)) {
682
+ return "NaN";
683
+ }
684
+
685
+ data = this._num_fmt.format(data as number);
686
+ break;
687
+ }
688
+
689
+ case "money": {
690
+ if (!isNumber(data)) {
691
+ return "NaN";
692
+ }
693
+
694
+ data = this._mny_fmt.format(data as number);
695
+ break;
696
+ }
697
+
698
+ case "percent": {
699
+ return new Box({
700
+ cls: "percent" + cls,
701
+ content: new Component({ cls: "bar", width: data + "%" })
702
+ });
703
+ }
704
+
705
+ case "icon": {
706
+ return new Icon({ cls, iconId: data + "" });
707
+ }
708
+
709
+ case "date": {
710
+ if( isString(data) ) {
711
+ data = new Date( data );
712
+ }
713
+
714
+ data = this._dte_fmt.format(data as Date);
715
+ break;
716
+ }
717
+
718
+ case "date-time": {
719
+ if( isString(data) ) {
720
+ data = new Date( data );
721
+ }
722
+
723
+ data = this._dtetme_fmt.format(data as Date);
724
+ break;
725
+ }
726
+
727
+ default: {
728
+ data = data + "";
729
+ break;
730
+ }
731
+ }
732
+
733
+ return new Component({ tag: "span", cls, content: data });
734
+ }
735
+
736
+ /**
737
+ *
738
+ */
739
+
740
+ private _buildRow(rowid: number, rec: DataRecord, top: number) {
741
+
742
+ const els: Component[] = [];
743
+ const count = this._getColCount();
744
+
745
+ for (let col = 0; col < count; col++) {
746
+ const cdata = this._getCol(col);
747
+ if (cdata.fixed) {
748
+ continue;
749
+ }
750
+
751
+ const extra: string[] = []
752
+ const content = this._renderCell(rec, cdata, extra );
753
+
754
+ let align = "start";
755
+ switch (cdata.align) {
756
+ default: align = "start"; break;
757
+ case "center": align = "center"; break;
758
+ case "right": align = "end"; break;
759
+ }
760
+
761
+ const el = new Component({
762
+ cls: "cell",
763
+ style: { width: cdata?.width ? cdata.width + "px" : undefined, justifyContent: align },
764
+ content
765
+ });
766
+
767
+ if( extra.length ) {
768
+ el.addClass( extra.join(' ') );
769
+ }
770
+
771
+ if (cdata.type) {
772
+ el.addClass(cdata.type);
773
+ }
774
+
775
+ el.setData("col", col + "");
776
+ el.setData("row", rowid + "")
777
+
778
+ els.push(el);
779
+ }
780
+
781
+ const rowel = new Box({ cls: "row", style: { top: top.toFixed(2) + "px" }, content: els });
782
+ rowel.setData("row", rowid + "");
783
+
784
+ if (this._selection.has(rowid)) {
785
+ rowel.addClass("selected");
786
+ }
787
+
788
+ return rowel;
789
+ }
790
+
791
+ /**
792
+ *
793
+ */
794
+
795
+ private _buildRowHeader(rowid: number, rec: DataRecord, top: number) {
796
+
797
+ const cols: Component[] = [];
798
+ const count = this._getColCount();
799
+
800
+ for (let col = 0; col < count; col++) {
801
+ const cdata = this._getCol(col);
802
+ if (!cdata?.fixed) {
803
+ continue;
804
+ }
805
+
806
+ const content = this._renderCell(rec, cdata.id, [cdata.type] );
807
+
808
+ let align = "start";
809
+ switch (cdata.align) {
810
+ default: align = "start"; break;
811
+ case "center": align = "center"; break;
812
+ case "right": align = "end"; break;
813
+ }
814
+
815
+ const el = new Component({
816
+ cls: "cell",
817
+ style: { width: cdata?.width ? cdata.width + "px" : undefined, justifyContent: align },
818
+ content
819
+ });
820
+
821
+ if (cdata.type) {
822
+ el.addClass(cdata.type);
823
+ }
824
+
825
+ el.setData("col", col + "");
826
+ el.setData("row", rowid + "")
827
+
828
+ cols.push(el);
829
+ }
830
+
831
+ const rowel = new Box({ cls: "row", style: { top: top + "px" }, content: cols });
832
+ rowel.setData("row", rowid + "");
833
+
834
+ if (this._selection.has(rowid)) {
835
+ rowel.addClass("selected");
836
+ }
837
+
838
+ return rowel;
839
+ }
840
+
841
+ /**
842
+ *
843
+ */
844
+
845
+ private _updateFlexs( ) {
846
+ let maxw = 0;
847
+ let flexc = 0;
848
+
849
+ const ccount = this._getColCount();
850
+
851
+ for (let x = 0; x < ccount; x++) {
852
+ const cdata = this._getCol(x);
853
+
854
+ if( !cdata.fixed && cdata.flex ) {
855
+ flexc += cdata.flex;
856
+ }
857
+ else {
858
+ maxw += cdata.width;
859
+ }
860
+ }
861
+
862
+ if( flexc ) {
863
+ const width = this._viewport.dom.clientWidth;
864
+ const delta = width - maxw;
865
+ const fw = delta / flexc;
866
+
867
+ for (let col = 0; col < ccount; col++) {
868
+ const cdata = this._getCol(col);
869
+ if( !cdata.fixed && cdata.flex ) {
870
+ cdata.width = Math.max( cdata.flex*fw, 32 );
871
+
872
+ const cols = this.queryAll(`[data-col="${col}"]`)
873
+ cols.forEach(c => {
874
+ c.setStyleValue("width", cdata.width + "px");
875
+ });
876
+ }
877
+ }
878
+ }
879
+ }
880
+
881
+ /**
882
+ *
883
+ */
884
+
885
+ private _computeFullSize() {
886
+
887
+ let maxw = 0;
888
+ let maxfw = 0;
889
+
890
+ const ccount = this._getColCount();
891
+
892
+ for (let x = 0; x < ccount; x++) {
893
+ const cdata = this._getCol(x);
894
+ let w = 0;
895
+
896
+ if (cdata.fixed) {
897
+ this._has_fixed = true;
898
+ }
899
+
900
+ if ( cdata.width) {
901
+ w += cdata.width;
902
+ }
903
+
904
+ if (cdata.fixed) {
905
+ maxfw += w;
906
+ }
907
+ else {
908
+ maxw += w;
909
+ }
910
+ }
911
+
912
+ const maxr = this._dataview ? this._dataview.getCount() : 0;
913
+ let maxh = maxr;
914
+
915
+ if (maxr < SCROLL_LIMIT) {
916
+ maxh *= this._row_height;
917
+ }
918
+ else {
919
+ const height = this._body.dom.parentElement.clientHeight;
920
+ const npage = height / this._row_height;
921
+ maxh = maxr - Math.floor(npage) + npage * this._row_height;
922
+ }
923
+
924
+ this.setStyleVariable("--fixed-width", maxfw + "px");
925
+ this._body.setStyleValue("height", maxh + "px");
926
+ this._body.setStyleValue("width", maxw + "px");
927
+ this._vheader.setStyleValue("height", maxh + "px");
928
+ }
929
+
930
+ /**
931
+ *
932
+ */
933
+
934
+ private _init() {
935
+ this._body = new Component({ cls: "body" });
936
+
937
+ this._viewport = new Viewport({ content: this._body });
938
+
939
+ if (!this._has_footer) {
940
+ this.setStyleVariable("--footer-height", "0");
941
+ }
942
+
943
+ // SCROLL
944
+ this._viewport.addDOMEvent( "scroll", (ev) => {
945
+ // sync horz & vert elements
946
+ this._left = this._viewport.dom.scrollLeft;
947
+ this.setStyleVariable("--left", -this._left + "px");
948
+
949
+ this._top = this._viewport.dom.scrollTop;
950
+ this.setStyleVariable("--top", -this._top + "px");
951
+
952
+ //this.setTimeout( "update", 0, ( ) => this._update( ) );
953
+ this._update()
954
+ });
955
+
956
+ // WHEEL
957
+ this.addDOMEvent("wheel", (ev: WheelEvent) => {
958
+ if (ev.deltaY && this._dataview && this._dataview.getCount() >= SCROLL_LIMIT) {
959
+ this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -1 : 1);
960
+ ev.stopPropagation();
961
+ ev.preventDefault();
962
+ }
963
+
964
+ if (this._has_fixed && ev.deltaY) {
965
+ // wheel on fixed part
966
+ // fixed part do not have scrollbar, so we need to handle it by hand
967
+ let t = ev.target as Node;
968
+ while (t != this.dom) {
969
+ if (t == this._vheader.dom) {
970
+ this._viewport.dom.scrollBy(0, ev.deltaY < 0 ? -this._row_height : this._row_height);
971
+ ev.stopPropagation();
972
+ ev.preventDefault();
973
+ break;
974
+ }
975
+
976
+ t = t.parentNode;
977
+ }
978
+ }
979
+ })
980
+
981
+ const targetRow = ( e: MouseEvent ) => {
982
+ let el = Component.parentElement(e.target as HTMLElement, Component);
983
+ while (el && !el.hasClass("row")) {
984
+ el = el.parentElement();
985
+ }
986
+
987
+ if (el) {
988
+ return el.getIntData("row");
989
+ }
990
+
991
+ return undefined;
992
+ }
993
+
994
+
995
+ // CLICK
996
+ this.addDOMEvent("click", (e) => {
997
+ const row = targetRow( e );
998
+ if (row!==undefined ) {
999
+ //TODO: multiselection
1000
+ if( !this._selection.has(row) ) {
1001
+ this._clearSelection();
1002
+ this._addSelection(row);
1003
+ }
1004
+ }
1005
+ });
1006
+
1007
+ // DBLCLICK
1008
+ this.addDOMEvent("dblclick", (e) => {
1009
+ const row = targetRow( e );
1010
+ if (row!==undefined ) {
1011
+ //TODO: multiselection
1012
+ if( !this._selection.has(row) ) {
1013
+ this._clearSelection();
1014
+ this._addSelection(row);
1015
+ }
1016
+
1017
+ this._on_dblclk( e, row );
1018
+
1019
+ const rec = this._dataview.getByIndex( row );
1020
+ this.fire( "dblClick", { context: rec } );
1021
+ }
1022
+ });
1023
+
1024
+ // CONTEXT
1025
+ this.addDOMEvent("contextmenu", (e) => {
1026
+ const row = targetRow( e );
1027
+ if (row!==undefined ) {
1028
+ //TODO: multiselection
1029
+ if( !this._selection.has(row) ) {
1030
+ this._clearSelection();
1031
+ this._addSelection(row);
1032
+ }
1033
+
1034
+ const rec = this._dataview.getByIndex( row );
1035
+ this.fire( "contextMenu", { uievent: e, context: rec } );
1036
+ }
1037
+
1038
+ e.preventDefault( );
1039
+ e.stopPropagation( );
1040
+ });
1041
+
1042
+ // MOUSE OVER
1043
+ this.addDOMEvent("mouseover", (e) => {
1044
+
1045
+ if (!this._has_fixed) {
1046
+ return;
1047
+ }
1048
+
1049
+ let el = Component.parentElement(e.target as HTMLElement, Component);
1050
+ while (el && !el.hasClass("row")) {
1051
+ el = el.parentElement();
1052
+ }
1053
+
1054
+ if (el) {
1055
+ const data = el.getData("row");
1056
+
1057
+ this.queryAll(".hover").forEach(x => x.removeClass("hover"));
1058
+
1059
+ if (data) {
1060
+ const rows = this.queryAll(`.row[data-row="${data}"]`);
1061
+ rows.forEach(x => x.addClass("hover"));
1062
+ }
1063
+ }
1064
+ });
1065
+
1066
+ // MOUSE LEAVE
1067
+ this.addDOMEvent("mouseleave", (e) => {
1068
+
1069
+ if (!this._has_fixed) {
1070
+ return;
1071
+ }
1072
+
1073
+ this.queryAll(".hover").forEach(x => x.removeClass("hover"));
1074
+ });
1075
+
1076
+ this._updateFlexs( );
1077
+
1078
+ this._fheader = this._buildColHeader(true);
1079
+ this._hheader = this._buildColHeader(false);
1080
+ this._vheader = new Box({ cls: "row-header" })
1081
+
1082
+ if (this._has_footer) {
1083
+ this._ffooter = this._buildColFooter(true);
1084
+ this._footer = this._buildColFooter(false);
1085
+ }
1086
+
1087
+ this.setContent([this._viewport, this._fheader, this._hheader, this._ffooter, this._footer, this._vheader]);
1088
+
1089
+ // compute misc variables
1090
+ {
1091
+ const rh = this.getStyleVariable("--row-height");
1092
+ this._row_height = parseInt(rh);
1093
+ }
1094
+
1095
+ this._computeFullSize();
1096
+ }
1097
+
1098
+ /**
1099
+ *
1100
+ */
1101
+
1102
+ protected _on_dblclk( e: UIEvent, row: number ) {
1103
+
1104
+ }
1105
+
1106
+ /**
1107
+ *
1108
+ */
1109
+
1110
+ private _update(force = false) {
1111
+
1112
+ if (!this._lock) {
1113
+ const rc = this.getBoundingRect();
1114
+
1115
+ // rows
1116
+ const rowc = this._dataview ? this._dataview.getCount() : 0;
1117
+ const mul = rowc < SCROLL_LIMIT ? this._row_height : 1;
1118
+
1119
+ const start = Math.floor(this._top / mul);
1120
+ const end = start + Math.ceil(rc.height / this._row_height);
1121
+ const hasFixed = this._has_fixed;
1122
+
1123
+ if (this._start != start || this._end != end || force) {
1124
+
1125
+ const rows: Component[] = [];
1126
+ const headers: Component[] = [];
1127
+
1128
+ if (force) {
1129
+ this._vis_rows.clear();
1130
+ }
1131
+
1132
+ let newvis: typeof this._vis_rows = new Map();
1133
+
1134
+ let y = start * mul;
1135
+
1136
+ for (let row = start; row < end && row < rowc; row++, y += this._row_height) {
1137
+
1138
+ let el = this._vis_rows.get(row);
1139
+ const rec = this._dataview.getByIndex(row);
1140
+
1141
+ if (hasFixed) {
1142
+ if (!el) {
1143
+ el = {
1144
+ h: this._buildRowHeader(row, rec, y),
1145
+ r: this._buildRow(row, rec, y),
1146
+ };
1147
+ }
1148
+ else {
1149
+ el.h.setStyleValue("top", y + "px");
1150
+ el.r.setStyleValue("top", y + "px");
1151
+ }
1152
+
1153
+ headers.push(el.h);
1154
+ }
1155
+ else {
1156
+ if (!el) {
1157
+ el = { h: null, r: this._buildRow(row, rec, y), };
1158
+ }
1159
+ else {
1160
+ el.r.setStyleValue("top", y + "px");
1161
+ }
1162
+ }
1163
+
1164
+ rows.push(el.r);
1165
+ newvis.set(row, el);
1166
+ }
1167
+
1168
+ if (hasFixed) {
1169
+ headers.push(new Component({ cls: "cell-out", style: { top: y + "px" } }));
1170
+ }
1171
+
1172
+ this._vis_rows = newvis;
1173
+ this._start = start;
1174
+ this._end = end;
1175
+
1176
+ this._body.setContent(rows);
1177
+
1178
+ if (hasFixed) {
1179
+ this._vheader.removeClass("@hidden");
1180
+ this._vheader.setContent(headers);
1181
+ }
1182
+ else {
1183
+ this._vheader.addClass("@hidden");
1184
+ }
1185
+ }
1186
+ }
1187
+ }
1188
+ /**
1189
+ *
1190
+ */
1191
+
1192
+ clearSelection( ) {
1193
+ this._clearSelection( );
1194
+ }
1195
+
1196
+ private _clearSelection() {
1197
+ for (const ref of this._selection.keys()) {
1198
+ const els = this.queryAll(`.row[data-row="${ref}"]`)
1199
+ els.forEach(el => {
1200
+ el.removeClass("selected");
1201
+ })
1202
+ }
1203
+
1204
+ this._selection.clear();
1205
+ }
1206
+
1207
+ /**
1208
+ *
1209
+ */
1210
+
1211
+ private _addSelection(rowid: number) {
1212
+ this._selection.add(rowid)
1213
+
1214
+ const els = this.queryAll(`.row[data-row="${rowid}"]`)
1215
+ els.forEach(el => {
1216
+ el.addClass("selected");
1217
+ });
1218
+
1219
+ const rec = this._dataview.getByIndex( rowid );
1220
+ this.fire("selectionChange", { selection: [rec], empty: false } );
1221
+ }
1222
+
1223
+ /**
1224
+ *
1225
+ */
1226
+
1227
+ getSelection( ) {
1228
+ if( this._selection.size==0 ) {
1229
+ return null;
1230
+ }
1231
+
1232
+ const ids = [...this._selection.values() ];
1233
+ return ids.map( id => this._dataview.getByIndex(id) );
1234
+ }
1235
+
1236
+ /**
1237
+ *
1238
+ */
1239
+
1240
+ getFirstSel( ) {
1241
+ if( this._selection.size==0 ) {
1242
+ return null;
1243
+ }
1244
+
1245
+ const id = this._selection.values().next().value;
1246
+ return this._dataview.getByIndex( id );
1247
+ }
1248
+
1249
+ /**
1250
+ *
1251
+ */
1252
+
1253
+ selectItem( id: any, ensureVisible = true ) {
1254
+ const index = this._dataview.indexOfId( id );
1255
+ if( index>=0 ) {
1256
+ this._addSelection( index );
1257
+ if( ensureVisible ) {
1258
+ this._scrollToIndex( index );
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ /**
1264
+ *
1265
+ */
1266
+
1267
+ setColTitle( col_name: any, title: string ) {
1268
+ const col = this._columns.findIndex( x => x.id==col_name );
1269
+ if( col>=0 ) {
1270
+ this._columns[col].title = title;
1271
+ const el = this._hheader.query<SimpleText>(`[data-col="${col}"] .title` )
1272
+ el.setText( title );
1273
+ }
1274
+ }
1275
+ }
1276
+