x4js 2.3.9 → 2.3.11
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.
- package/aicontext.md +70 -3
- package/aiguide.md +60 -0
- package/package.json +1 -1
- package/src/components/autocomplete/autocomplete.module.scss +28 -0
- package/src/components/autocomplete/autocomplete.ts +361 -0
- package/src/components/boxes/boxes.ts +66 -0
- package/src/components/button/button.ts +1 -1
- package/src/components/chart/chart.module.scss +126 -0
- package/src/components/chart/chart.ts +758 -0
- package/src/components/combobox/combobox.ts +5 -5
- package/src/components/components.ts +2 -0
- package/src/components/header/header.ts +38 -3
- package/src/components/listbox/listbox.module.scss +4 -5
- package/src/components/listbox/listbox.ts +1 -1
- package/src/components/select/select.module.scss +26 -17
- package/src/components/select/select.ts +60 -10
- package/src/components/spreadsheet/spreadsheet.module.scss +31 -0
- package/src/components/spreadsheet/spreadsheet.ts +522 -11
- package/src/components/textarea/textarea.ts +2 -2
- package/src/components/textedit/textedit.ts +4 -4
- package/src/core/core_application.ts +1 -0
- package/src/core/core_svg.ts +2 -2
- package/readme.npm.md +0 -248
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
**/
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
import { Component, ComponentContent, ComponentEvents, ComponentProps, EvChange, EvClick, EvContextMenu, EvDblClick, EvSelectionChange, componentFromDOM } from '../../core/component';
|
|
18
|
+
import { Component, ComponentContent, ComponentEvent, ComponentEvents, ComponentProps, EvChange, EvClick, EvContextMenu, EvDblClick, EvSelectionChange, componentFromDOM } from '../../core/component';
|
|
19
19
|
import { GridColumn } from '../gridview/gridview'
|
|
20
20
|
|
|
21
|
-
import { class_ns, isNumber, isString, UnsafeHtml } from '../../core/core_tools';
|
|
21
|
+
import { class_ns, IFormElement, isFunction, isNumber, isString, UnsafeHtml } from '../../core/core_tools';
|
|
22
22
|
import { CoreEvent, EventCallback, EventMap } from '../../core/core_events';
|
|
23
23
|
import { kbNav } from '../../core/core_tools';
|
|
24
24
|
|
|
@@ -28,6 +28,7 @@ import { Box } from '../boxes/boxes';
|
|
|
28
28
|
import { CSizer } from '../sizers/sizer'
|
|
29
29
|
import { Viewport } from '../viewport/viewport';
|
|
30
30
|
import { SimpleText } from '../label/label';
|
|
31
|
+
import { Input } from '../input/input';
|
|
31
32
|
import { CoreElement } from '../../x4.js';
|
|
32
33
|
|
|
33
34
|
import icons from "../assets/icons"
|
|
@@ -42,8 +43,39 @@ export type SSCellClassifier = ( row: number, col: number ) => string; // re
|
|
|
42
43
|
export type RowClassifier = (row: number ) => string; // return the row computed class
|
|
43
44
|
//export type CellRenderer = (row: number, col: number, content: any) => Component;
|
|
44
45
|
|
|
46
|
+
export type SSCellEditable = ( row: number, col: number ) => boolean; // col is the column index
|
|
47
|
+
export type SSCellEditor = ( row: number, col: number, value: any ) => Component; // col is the column index
|
|
48
|
+
|
|
45
49
|
export interface SpreadsheetColumn extends Omit<GridColumn,"classifier"> {
|
|
46
50
|
cellClassifier?: SSCellClassifier;
|
|
51
|
+
|
|
52
|
+
/** allow the inline edition of the cells of this column */
|
|
53
|
+
editable?: boolean | SSCellEditable;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* custom cell editor (default is an input matching the column type)
|
|
57
|
+
* the component must implement the "form-element" interface
|
|
58
|
+
*/
|
|
59
|
+
editor?: SSCellEditor;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* fired before an edited value is written to the store
|
|
64
|
+
* call preventDefault to refuse the value
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
export interface EvCellChange extends ComponentEvent {
|
|
68
|
+
row: number;
|
|
69
|
+
col: number; // column index
|
|
70
|
+
colId: any; // column id (the key in the store)
|
|
71
|
+
value: any; // new value, the handler can change it
|
|
72
|
+
oldValue: any;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface CellEdit extends CellRef {
|
|
76
|
+
editor: Component;
|
|
77
|
+
initial?: string; // text of the editor at start
|
|
78
|
+
busy?: boolean; // rows are being rebuilt, focus loss must be ignored
|
|
47
79
|
}
|
|
48
80
|
|
|
49
81
|
|
|
@@ -181,6 +213,7 @@ export interface SpreadsheetEvents extends ComponentEvents {
|
|
|
181
213
|
dblClick?: EvDblClick;
|
|
182
214
|
contextMenu?: EvContextMenu;
|
|
183
215
|
selectionChange?: EvSelectionChange;
|
|
216
|
+
cellChange?: EvCellChange;
|
|
184
217
|
}
|
|
185
218
|
|
|
186
219
|
export interface SpreadsheetProps extends ComponentProps {
|
|
@@ -189,7 +222,10 @@ export interface SpreadsheetProps extends ComponentProps {
|
|
|
189
222
|
columns: SpreadsheetColumn[];
|
|
190
223
|
rowClassifier?: RowClassifier;
|
|
191
224
|
|
|
225
|
+
/** where the selection goes when an edition is validated with Enter (default "down") */
|
|
226
|
+
enterMove?: "down" | "right" | "none";
|
|
192
227
|
|
|
228
|
+
cellChange?: EventCallback<EvCellChange>;
|
|
193
229
|
click?: EventCallback<EvClick>;
|
|
194
230
|
dblClick?: EventCallback<EvDblClick>;
|
|
195
231
|
contextMenu?: EventCallback<EvContextMenu>;
|
|
@@ -267,6 +303,8 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
267
303
|
private _has_fixed: boolean;
|
|
268
304
|
private _has_footer: boolean;
|
|
269
305
|
|
|
306
|
+
private _edit: CellEdit; // cell being edited
|
|
307
|
+
|
|
270
308
|
constructor(props: P) {
|
|
271
309
|
super(props);
|
|
272
310
|
|
|
@@ -285,7 +323,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
285
323
|
|
|
286
324
|
this._columns = props.columns.map(x => x);
|
|
287
325
|
|
|
288
|
-
this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange");
|
|
326
|
+
this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange", "cellChange");
|
|
289
327
|
|
|
290
328
|
this.lock(true);
|
|
291
329
|
this.setAttribute("tabindex", 0);
|
|
@@ -322,6 +360,11 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
322
360
|
return;
|
|
323
361
|
}
|
|
324
362
|
|
|
363
|
+
// keys of the cell editor are handled by the editor
|
|
364
|
+
if( this._edit ) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
325
368
|
switch (ev.key) {
|
|
326
369
|
case "ArrowDown": {
|
|
327
370
|
this.navigate(kbNav.next);
|
|
@@ -363,8 +406,43 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
363
406
|
break;
|
|
364
407
|
}
|
|
365
408
|
|
|
366
|
-
|
|
409
|
+
case "F2":
|
|
410
|
+
case "Enter": {
|
|
411
|
+
const sel = this._curCell( );
|
|
412
|
+
if( !sel || !this.editCell( sel.row, sel.col ) ) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
case "Delete": {
|
|
419
|
+
const sel = this._curCell( );
|
|
420
|
+
if( !sel || !this._isEditable( sel.row, sel.col ) ) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this._setCellValue( sel.row, sel.col, null );
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
default: {
|
|
429
|
+
const sel = this._curCell( );
|
|
430
|
+
if( !sel ) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if( ev.key==" " && this._toggleCell( sel.row, sel.col ) ) {
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// a typed char starts the edition: the editor takes the focus, so the char goes into it
|
|
439
|
+
const altgr = ev.getModifierState( "AltGraph" );
|
|
440
|
+
if( ev.key.length==1 && ( altgr || ( !ev.ctrlKey && !ev.altKey && !ev.metaKey ) ) ) {
|
|
441
|
+
this.editCell( sel.row, sel.col, true );
|
|
442
|
+
}
|
|
443
|
+
|
|
367
444
|
return;
|
|
445
|
+
}
|
|
368
446
|
}
|
|
369
447
|
|
|
370
448
|
ev.preventDefault();
|
|
@@ -391,8 +469,8 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
391
469
|
|
|
392
470
|
this._selection.forEach( x => {
|
|
393
471
|
const row = x>>12;
|
|
394
|
-
if( m===undefined || m>row ) { m = row; col=x&
|
|
395
|
-
if( M===undefined || M<row ) { M = row; col=x&
|
|
472
|
+
if( m===undefined || m>row ) { m = row; col=x&0xfff; }
|
|
473
|
+
if( M===undefined || M<row ) { M = row; col=x&0xfff; }
|
|
396
474
|
} );
|
|
397
475
|
|
|
398
476
|
return [top ? m : M, col]
|
|
@@ -450,7 +528,13 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
450
528
|
if (ncol >= 0 && ncol < this._columns.length ) {
|
|
451
529
|
this._clearSelection(false);
|
|
452
530
|
this._addSelection( mkid(fline,ncol), true);
|
|
453
|
-
|
|
531
|
+
|
|
532
|
+
// fixed columns are always visible
|
|
533
|
+
if( !this._getCol(ncol).fixed ) {
|
|
534
|
+
const cell = this.query( `.cell[data-ref="${mkid(fline,ncol)}"]` );
|
|
535
|
+
cell?.scrollIntoView( { block: "nearest", inline: "nearest" } );
|
|
536
|
+
}
|
|
537
|
+
|
|
454
538
|
return true;
|
|
455
539
|
}
|
|
456
540
|
}
|
|
@@ -497,7 +581,9 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
497
581
|
if (ev.type == 'changed' && this._selection.size ) {
|
|
498
582
|
const nsel = new Set<number>();
|
|
499
583
|
this._selection.forEach(x => {
|
|
500
|
-
|
|
584
|
+
// selection use column index, store use column id
|
|
585
|
+
const cdata = this._getCol( x & 0xfff );
|
|
586
|
+
if( cdata && this._store.hasData( x >> 12, cdata.id ) ) {
|
|
501
587
|
nsel.add( x );
|
|
502
588
|
}
|
|
503
589
|
});
|
|
@@ -857,7 +943,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
857
943
|
el.setInternalData("row", rowid);
|
|
858
944
|
el.setData("ref", mkid(rowid, col) + "");
|
|
859
945
|
|
|
860
|
-
if (this._selection.has(mkid(
|
|
946
|
+
if (this._selection.has(mkid(rowid, col))) {
|
|
861
947
|
el.addClass("selected");
|
|
862
948
|
}
|
|
863
949
|
|
|
@@ -1028,8 +1114,33 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1028
1114
|
}
|
|
1029
1115
|
|
|
1030
1116
|
|
|
1117
|
+
// MOUSEDOWN
|
|
1118
|
+
this.addDOMEvent("mousedown", (e) => {
|
|
1119
|
+
if( !this._edit || this._inEditor(e) ) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// click on another cell while editing:
|
|
1124
|
+
// validate now, rows may be rebuilt before the click event (and the click lost)
|
|
1125
|
+
const ref = targetCell(e);
|
|
1126
|
+
if( ref && ref.row!==undefined ) {
|
|
1127
|
+
this._endEdit( true, true );
|
|
1128
|
+
|
|
1129
|
+
if (!this._selection.has(ref.ref)) {
|
|
1130
|
+
this._clearSelection( false );
|
|
1131
|
+
this._addSelection(ref.ref,true);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
e.preventDefault( );
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1031
1138
|
// CLICK
|
|
1032
1139
|
this.addDOMEvent("click", (e) => {
|
|
1140
|
+
if( this._inEditor(e) ) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1033
1144
|
const ref = targetCell(e);
|
|
1034
1145
|
if (ref) {
|
|
1035
1146
|
//TODO: multiselection
|
|
@@ -1037,11 +1148,17 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1037
1148
|
this._clearSelection( false );
|
|
1038
1149
|
this._addSelection(ref.ref,true);
|
|
1039
1150
|
}
|
|
1151
|
+
|
|
1152
|
+
this._toggleCell( ref.row, ref.col );
|
|
1040
1153
|
}
|
|
1041
1154
|
});
|
|
1042
1155
|
|
|
1043
1156
|
// DBLCLICK
|
|
1044
1157
|
this.addDOMEvent("dblclick", (e) => {
|
|
1158
|
+
if( this._inEditor(e) ) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1045
1162
|
const ref = targetCell(e);
|
|
1046
1163
|
if (ref) {
|
|
1047
1164
|
//TODO: multiselection
|
|
@@ -1050,12 +1167,22 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1050
1167
|
this._addSelection(ref.ref,true);
|
|
1051
1168
|
}
|
|
1052
1169
|
|
|
1053
|
-
|
|
1170
|
+
const dev: EvDblClick = { context: { row: ref.row, col: ref.col } };
|
|
1171
|
+
this.fire( "dblClick", dev );
|
|
1172
|
+
|
|
1173
|
+
if( !dev.defaultPrevented ) {
|
|
1174
|
+
this.editCell( ref.row, ref.col );
|
|
1175
|
+
}
|
|
1054
1176
|
}
|
|
1055
1177
|
});
|
|
1056
1178
|
|
|
1057
1179
|
// CONTEXT
|
|
1058
1180
|
this.addDOMEvent("contextmenu", (e) => {
|
|
1181
|
+
// keep the native menu of the editor (copy/paste)
|
|
1182
|
+
if( this._inEditor(e) ) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1059
1186
|
const ref = targetCell(e);
|
|
1060
1187
|
if (ref) {
|
|
1061
1188
|
//TODO: multiselection
|
|
@@ -1115,6 +1242,13 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1115
1242
|
|
|
1116
1243
|
if (this._start != start || this._end != end || force) {
|
|
1117
1244
|
|
|
1245
|
+
// rows are rebuilt: the cell editor must survive
|
|
1246
|
+
const edit = this._edit;
|
|
1247
|
+
const edit_focus = edit ? edit.editor.dom.contains( document.activeElement ) : false;
|
|
1248
|
+
if( edit ) {
|
|
1249
|
+
edit.busy = true;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1118
1252
|
const rows: Component[] = [];
|
|
1119
1253
|
const headers: Component[] = [];
|
|
1120
1254
|
|
|
@@ -1175,6 +1309,10 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1175
1309
|
else {
|
|
1176
1310
|
this._vheader.addClass("@hidden");
|
|
1177
1311
|
}
|
|
1312
|
+
|
|
1313
|
+
if( edit && this._edit===edit ) {
|
|
1314
|
+
this._restoreEditor( edit, edit_focus );
|
|
1315
|
+
}
|
|
1178
1316
|
}
|
|
1179
1317
|
}
|
|
1180
1318
|
}
|
|
@@ -1211,7 +1349,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1211
1349
|
|
|
1212
1350
|
if (notify) {
|
|
1213
1351
|
const selection = this.getSelection();
|
|
1214
|
-
this.fire("selectionChange", { selection, empty: selection.length
|
|
1352
|
+
this.fire("selectionChange", { selection, empty: selection.length == 0 });
|
|
1215
1353
|
}
|
|
1216
1354
|
}
|
|
1217
1355
|
|
|
@@ -1243,5 +1381,378 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1243
1381
|
|
|
1244
1382
|
this._addSelection(mkid(row, col), true);
|
|
1245
1383
|
}
|
|
1384
|
+
|
|
1385
|
+
// :: EDITION ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* @returns the selected cell when there is exactly one
|
|
1389
|
+
*/
|
|
1390
|
+
|
|
1391
|
+
private _curCell( ): CellRef {
|
|
1392
|
+
if( this._selection.size!=1 ) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const ref = this._selection.values().next().value as number;
|
|
1397
|
+
return { row: ref >> 12, col: ref & 0xfff };
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
private _inEditor( e: Event ) {
|
|
1401
|
+
return !!this._edit && this._edit.editor.dom.contains( e.target as Node );
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
*
|
|
1406
|
+
*/
|
|
1407
|
+
|
|
1408
|
+
private _isEditable( row: number, col: number ) {
|
|
1409
|
+
const cdata = this._getCol( col );
|
|
1410
|
+
if( !cdata?.editable || !this._store || this.isDisabled() ) {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
if( !(row>=0) || row>=this._store.getRowCount() ) {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if( isFunction(cdata.editable) && !cdata.editable( row, col ) ) {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// html content & images cannot be edited by the default editor
|
|
1423
|
+
if( !cdata.editor ) {
|
|
1424
|
+
if( cdata.type=="image" || cdata.type=="icon" ) {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if( this._store.getData( row, cdata.id ) instanceof UnsafeHtml ) {
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* write a value to the store (after cellChange agreement)
|
|
1438
|
+
* @returns true if the value was written
|
|
1439
|
+
*/
|
|
1440
|
+
|
|
1441
|
+
private _setCellValue( row: number, col: number, value: any ) {
|
|
1442
|
+
const cdata = this._getCol( col );
|
|
1443
|
+
if( !cdata || !this._store || row>=this._store.getRowCount() ) {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const empty = ( v: any ) => v===undefined || v===null || v==="";
|
|
1448
|
+
const same = ( a: any, b: any ) => {
|
|
1449
|
+
if( a instanceof Date && b instanceof Date ) {
|
|
1450
|
+
return a.getTime()==b.getTime();
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
return a===b || ( empty(a) && empty(b) );
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const oldValue = this._store.getData( row, cdata.id );
|
|
1457
|
+
if( same( value, oldValue ) ) {
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const ev: EvCellChange = { row, col, colId: cdata.id, value, oldValue };
|
|
1462
|
+
this.fire( "cellChange", ev );
|
|
1463
|
+
|
|
1464
|
+
if( ev.defaultPrevented ) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
this._store.setData( row, cdata.id, ev.value );
|
|
1469
|
+
return true;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* editable checkboxes are toggled without editor
|
|
1474
|
+
*/
|
|
1475
|
+
|
|
1476
|
+
private _toggleCell( row: number, col: number ) {
|
|
1477
|
+
const cdata = this._getCol( col );
|
|
1478
|
+
if( cdata?.type!="checkbox" || cdata.editor || !this._isEditable( row, col ) ) {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
this._setCellValue( row, col, !this._store.getData( row, cdata.id ) );
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* default editor: an input matching the column type
|
|
1488
|
+
*/
|
|
1489
|
+
|
|
1490
|
+
private _createEditor( cdata: SpreadsheetColumn, value: any, clear: boolean ): Component {
|
|
1491
|
+
switch( cdata.type ) {
|
|
1492
|
+
case "number":
|
|
1493
|
+
case "money":
|
|
1494
|
+
case "percent": {
|
|
1495
|
+
const ed = new Input( { type: "number", value: !clear && isNumber(value) ? value : undefined } );
|
|
1496
|
+
ed.setAttribute( "step", "any" );
|
|
1497
|
+
return ed;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
case "date": {
|
|
1501
|
+
return new Input( { type: "date", value: !clear && value instanceof Date ? value : undefined } );
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
default: {
|
|
1505
|
+
return new Input( { type: "text", value: clear || value===undefined || value===null ? "" : value+"" } );
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* @returns the value of the editor, undefined if there is nothing to write
|
|
1512
|
+
*/
|
|
1513
|
+
|
|
1514
|
+
private _readEditor( edit: CellEdit ): any {
|
|
1515
|
+
const cdata = this._getCol( edit.col );
|
|
1516
|
+
if( !cdata ) {
|
|
1517
|
+
return undefined;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if( cdata.editor ) {
|
|
1521
|
+
const fe = edit.editor.queryInterface<IFormElement>( "form-element" );
|
|
1522
|
+
return fe && fe.isValid() ? fe.getRawValue( ) : undefined;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const input = edit.editor as Input;
|
|
1526
|
+
if( (input.dom as HTMLInputElement).validity?.badInput ) {
|
|
1527
|
+
return undefined;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const text = input.getValue( );
|
|
1531
|
+
if( text===edit.initial ) {
|
|
1532
|
+
return undefined; // untouched
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
switch( cdata.type ) {
|
|
1536
|
+
case "number":
|
|
1537
|
+
case "money":
|
|
1538
|
+
case "percent": {
|
|
1539
|
+
const v = parseFloat( text );
|
|
1540
|
+
return isNaN(v) ? null : v;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
case "date": {
|
|
1544
|
+
const [y,m,d] = text.split( "-" ).map( x => parseInt(x) );
|
|
1545
|
+
return y ? new Date( y, m-1, d ) : null;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
default: {
|
|
1549
|
+
// keep numbers as numbers
|
|
1550
|
+
const old = this._store.getData( edit.row, cdata.id );
|
|
1551
|
+
if( isNumber(old) && text!=="" && isFinite( Number(text) ) ) {
|
|
1552
|
+
return Number( text );
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
return text;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
private _focusEditor( editor: Component ) {
|
|
1561
|
+
const dom = editor.dom as HTMLElement;
|
|
1562
|
+
const el = dom.matches( "input,select,textarea" ) ? dom : dom.querySelector<HTMLElement>( "input,select,textarea,[tabindex]" );
|
|
1563
|
+
( el ?? dom ).focus( { preventScroll: true } );
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* rows have been rebuilt: put the editor back in its cell
|
|
1568
|
+
*/
|
|
1569
|
+
|
|
1570
|
+
private _restoreEditor( edit: CellEdit, focus: boolean ) {
|
|
1571
|
+
const cell = this.query( `.cell[data-ref="${mkid(edit.row,edit.col)}"]` );
|
|
1572
|
+
if( !cell ) {
|
|
1573
|
+
// the cell is no more visible
|
|
1574
|
+
edit.busy = false;
|
|
1575
|
+
this._endEdit( true, focus );
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
if( edit.editor.dom.parentElement!=cell.dom ) {
|
|
1580
|
+
cell.addClass( "editing" );
|
|
1581
|
+
cell.appendContent( edit.editor );
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
if( focus ) {
|
|
1585
|
+
this._focusEditor( edit.editor );
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
edit.busy = false;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
*
|
|
1593
|
+
*/
|
|
1594
|
+
|
|
1595
|
+
private _on_edit_key( ev: KeyboardEvent ) {
|
|
1596
|
+
// the grid must not handle the editor keys (navigation)
|
|
1597
|
+
ev.stopPropagation( );
|
|
1598
|
+
|
|
1599
|
+
if( ev.isComposing ) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
switch( ev.key ) {
|
|
1604
|
+
case "Enter": {
|
|
1605
|
+
this._endEdit( true, true );
|
|
1606
|
+
|
|
1607
|
+
const move = this.props.enterMove ?? "down";
|
|
1608
|
+
if( move=="down" ) {
|
|
1609
|
+
this.navigate( ev.shiftKey ? kbNav.prev : kbNav.next );
|
|
1610
|
+
}
|
|
1611
|
+
else if( move=="right" ) {
|
|
1612
|
+
this.navigate( ev.shiftKey ? kbNav.left : kbNav.right );
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
break;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
case "Tab": {
|
|
1619
|
+
this._endEdit( true, true );
|
|
1620
|
+
this.navigate( ev.shiftKey ? kbNav.left : kbNav.right );
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
case "Escape": {
|
|
1625
|
+
this._endEdit( false, true );
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
default:
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
ev.preventDefault( );
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* @param refocus - give the focus back to the grid (default: only if the editor has it)
|
|
1638
|
+
*/
|
|
1639
|
+
|
|
1640
|
+
private _endEdit( commit: boolean, refocus?: boolean ) {
|
|
1641
|
+
const edit = this._edit;
|
|
1642
|
+
if( !edit ) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
this._edit = null;
|
|
1647
|
+
|
|
1648
|
+
const value = commit ? this._readEditor( edit ) : undefined;
|
|
1649
|
+
const dom = edit.editor.dom;
|
|
1650
|
+
|
|
1651
|
+
if( refocus===undefined ) {
|
|
1652
|
+
refocus = dom.contains( document.activeElement );
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
const cell = componentFromDOM( dom.parentElement );
|
|
1656
|
+
cell?.removeClass( "editing" );
|
|
1657
|
+
dom.remove( );
|
|
1658
|
+
|
|
1659
|
+
if( refocus ) {
|
|
1660
|
+
(this.dom as HTMLElement).focus( { preventScroll: true } );
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if( value!==undefined ) {
|
|
1664
|
+
this._setCellValue( edit.row, edit.col, value );
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* start the edition of a cell (the cell must be visible and its column editable)
|
|
1670
|
+
* @param row - row index
|
|
1671
|
+
* @param col - column index
|
|
1672
|
+
* @param clear - start with an empty editor
|
|
1673
|
+
* @returns false if the cell cannot be edited
|
|
1674
|
+
*/
|
|
1675
|
+
|
|
1676
|
+
editCell( row: number, col: number, clear = false ): boolean {
|
|
1677
|
+
this._endEdit( true );
|
|
1678
|
+
|
|
1679
|
+
const cdata = this._getCol( col );
|
|
1680
|
+
if( !this._isEditable( row, col ) || ( cdata.type=="checkbox" && !cdata.editor ) ) {
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const ref = mkid( row, col );
|
|
1685
|
+
const cell = this.query( `.cell[data-ref="${ref}"]` );
|
|
1686
|
+
if( !cell ) {
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if( this._selection.size!=1 || !this._selection.has(ref) ) {
|
|
1691
|
+
this._clearSelection( false );
|
|
1692
|
+
this._addSelection( ref, true );
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const value = this._store.getData( row, cdata.id );
|
|
1696
|
+
const editor = cdata.editor ? cdata.editor( row, col, value ) : this._createEditor( cdata, value, clear );
|
|
1697
|
+
if( !editor ) {
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const edit: CellEdit = { row, col, editor };
|
|
1702
|
+
if( !cdata.editor ) {
|
|
1703
|
+
edit.initial = (editor as Input).getValue( );
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
editor.addClass( "cell-editor" );
|
|
1707
|
+
editor.addDOMEvent( "keydown", ( ev ) => this._on_edit_key( ev ) );
|
|
1708
|
+
|
|
1709
|
+
// avoid a second dispatch of the editor handlers by the grid
|
|
1710
|
+
editor.addDOMEvent( "wheel", ( ev ) => ev.stopPropagation( ) );
|
|
1711
|
+
|
|
1712
|
+
editor.addDOMEvent( "focusout", ( ev ) => {
|
|
1713
|
+
if( this._edit!==edit || edit.busy ) {
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// focus is moving inside the editor
|
|
1718
|
+
const to = ev.relatedTarget as Node;
|
|
1719
|
+
if( to && editor.dom.contains(to) ) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
this._endEdit( true, false );
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
this._edit = edit;
|
|
1727
|
+
|
|
1728
|
+
cell.addClass( "editing" );
|
|
1729
|
+
cell.appendContent( editor );
|
|
1730
|
+
this._focusEditor( editor );
|
|
1731
|
+
|
|
1732
|
+
if( !clear && !cdata.editor ) {
|
|
1733
|
+
const input = editor as Input;
|
|
1734
|
+
if( cdata.type=="number" || cdata.type=="money" || cdata.type=="percent" ) {
|
|
1735
|
+
input.selectAll( );
|
|
1736
|
+
}
|
|
1737
|
+
else if( cdata.type!="date" ) {
|
|
1738
|
+
input.select( input.getValue().length, 0 );
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
return true;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* stop the current edition
|
|
1747
|
+
* @param commit - false to cancel
|
|
1748
|
+
*/
|
|
1749
|
+
|
|
1750
|
+
stopEdit( commit = true ) {
|
|
1751
|
+
this._endEdit( commit );
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
isEditing( ) {
|
|
1755
|
+
return !!this._edit;
|
|
1756
|
+
}
|
|
1246
1757
|
}
|
|
1247
1758
|
|
|
@@ -50,7 +50,7 @@ export class SimpleTextArea extends Component<TextAreaProps> {
|
|
|
50
50
|
|
|
51
51
|
this.setAttribute( "name", props.name );
|
|
52
52
|
if( props.value ) {
|
|
53
|
-
this.
|
|
53
|
+
this.setText( props.value+'' );
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
if( props.resize!==undefined ) {
|
|
@@ -119,7 +119,7 @@ export class TextArea extends VBox {
|
|
|
119
119
|
|
|
120
120
|
this.setContent( [
|
|
121
121
|
new Label( { text: props.label }),
|
|
122
|
-
this._input = new SimpleTextArea( { label,value,resize,readonly,trim } )
|
|
122
|
+
this._input = new SimpleTextArea( { label,value,resize,readonly,trim,name } )
|
|
123
123
|
])
|
|
124
124
|
}
|
|
125
125
|
|