x4js 2.3.10 → 2.3.12
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/README.md +13 -1
- package/aicli.md +194 -0
- package/aicontext.md +75 -3
- package/aiguide.md +67 -0
- package/cli/main.mjs +3 -1
- package/package.json +2 -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 +546 -11
- package/src/components/textedit/textedit.ts +4 -4
- package/src/core/core_router.ts +15 -3
- 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,51 @@ 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
|
+
/**
|
|
76
|
+
* fired for each key pressed on the grid (not while a cell is edited), before the default handling
|
|
77
|
+
* call preventDefault to handle the key yourself
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
export interface EvCellKey extends ComponentEvent {
|
|
81
|
+
key: string; // "F2", "Enter", "a"...
|
|
82
|
+
row: number; // selected cell, undefined if none
|
|
83
|
+
col: number; // column index
|
|
84
|
+
uievent: KeyboardEvent; // UI event that fire this event
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CellEdit extends CellRef {
|
|
88
|
+
editor: Component;
|
|
89
|
+
initial?: string; // text of the editor at start
|
|
90
|
+
busy?: boolean; // rows are being rebuilt, focus loss must be ignored
|
|
47
91
|
}
|
|
48
92
|
|
|
49
93
|
|
|
@@ -181,6 +225,8 @@ export interface SpreadsheetEvents extends ComponentEvents {
|
|
|
181
225
|
dblClick?: EvDblClick;
|
|
182
226
|
contextMenu?: EvContextMenu;
|
|
183
227
|
selectionChange?: EvSelectionChange;
|
|
228
|
+
cellChange?: EvCellChange;
|
|
229
|
+
cellKey?: EvCellKey;
|
|
184
230
|
}
|
|
185
231
|
|
|
186
232
|
export interface SpreadsheetProps extends ComponentProps {
|
|
@@ -189,7 +235,11 @@ export interface SpreadsheetProps extends ComponentProps {
|
|
|
189
235
|
columns: SpreadsheetColumn[];
|
|
190
236
|
rowClassifier?: RowClassifier;
|
|
191
237
|
|
|
238
|
+
/** where the selection goes when an edition is validated with Enter (default "down") */
|
|
239
|
+
enterMove?: "down" | "right" | "none";
|
|
192
240
|
|
|
241
|
+
cellChange?: EventCallback<EvCellChange>;
|
|
242
|
+
cellKey?: EventCallback<EvCellKey>;
|
|
193
243
|
click?: EventCallback<EvClick>;
|
|
194
244
|
dblClick?: EventCallback<EvDblClick>;
|
|
195
245
|
contextMenu?: EventCallback<EvContextMenu>;
|
|
@@ -267,6 +317,8 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
267
317
|
private _has_fixed: boolean;
|
|
268
318
|
private _has_footer: boolean;
|
|
269
319
|
|
|
320
|
+
private _edit: CellEdit; // cell being edited
|
|
321
|
+
|
|
270
322
|
constructor(props: P) {
|
|
271
323
|
super(props);
|
|
272
324
|
|
|
@@ -285,7 +337,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
285
337
|
|
|
286
338
|
this._columns = props.columns.map(x => x);
|
|
287
339
|
|
|
288
|
-
this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange");
|
|
340
|
+
this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange", "cellChange", "cellKey");
|
|
289
341
|
|
|
290
342
|
this.lock(true);
|
|
291
343
|
this.setAttribute("tabindex", 0);
|
|
@@ -322,6 +374,22 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
322
374
|
return;
|
|
323
375
|
}
|
|
324
376
|
|
|
377
|
+
// keys of the cell editor are handled by the editor
|
|
378
|
+
if( this._edit ) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// the application first
|
|
383
|
+
const cur = this._curCell( );
|
|
384
|
+
const kev: EvCellKey = { key: ev.key, row: cur?.row, col: cur?.col, uievent: ev };
|
|
385
|
+
this.fire( "cellKey", kev );
|
|
386
|
+
|
|
387
|
+
if( kev.defaultPrevented ) {
|
|
388
|
+
ev.preventDefault();
|
|
389
|
+
ev.stopPropagation();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
325
393
|
switch (ev.key) {
|
|
326
394
|
case "ArrowDown": {
|
|
327
395
|
this.navigate(kbNav.next);
|
|
@@ -363,8 +431,43 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
363
431
|
break;
|
|
364
432
|
}
|
|
365
433
|
|
|
366
|
-
|
|
434
|
+
case "F2":
|
|
435
|
+
case "Enter": {
|
|
436
|
+
const sel = this._curCell( );
|
|
437
|
+
if( !sel || !this.editCell( sel.row, sel.col ) ) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
case "Delete": {
|
|
444
|
+
const sel = this._curCell( );
|
|
445
|
+
if( !sel || !this._isEditable( sel.row, sel.col ) ) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this._setCellValue( sel.row, sel.col, null );
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
default: {
|
|
454
|
+
const sel = this._curCell( );
|
|
455
|
+
if( !sel ) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if( ev.key==" " && this._toggleCell( sel.row, sel.col ) ) {
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// a typed char starts the edition: the editor takes the focus, so the char goes into it
|
|
464
|
+
const altgr = ev.getModifierState( "AltGraph" );
|
|
465
|
+
if( ev.key.length==1 && ( altgr || ( !ev.ctrlKey && !ev.altKey && !ev.metaKey ) ) ) {
|
|
466
|
+
this.editCell( sel.row, sel.col, true );
|
|
467
|
+
}
|
|
468
|
+
|
|
367
469
|
return;
|
|
470
|
+
}
|
|
368
471
|
}
|
|
369
472
|
|
|
370
473
|
ev.preventDefault();
|
|
@@ -391,8 +494,8 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
391
494
|
|
|
392
495
|
this._selection.forEach( x => {
|
|
393
496
|
const row = x>>12;
|
|
394
|
-
if( m===undefined || m>row ) { m = row; col=x&
|
|
395
|
-
if( M===undefined || M<row ) { M = row; col=x&
|
|
497
|
+
if( m===undefined || m>row ) { m = row; col=x&0xfff; }
|
|
498
|
+
if( M===undefined || M<row ) { M = row; col=x&0xfff; }
|
|
396
499
|
} );
|
|
397
500
|
|
|
398
501
|
return [top ? m : M, col]
|
|
@@ -450,7 +553,13 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
450
553
|
if (ncol >= 0 && ncol < this._columns.length ) {
|
|
451
554
|
this._clearSelection(false);
|
|
452
555
|
this._addSelection( mkid(fline,ncol), true);
|
|
453
|
-
|
|
556
|
+
|
|
557
|
+
// fixed columns are always visible
|
|
558
|
+
if( !this._getCol(ncol).fixed ) {
|
|
559
|
+
const cell = this.query( `.cell[data-ref="${mkid(fline,ncol)}"]` );
|
|
560
|
+
cell?.scrollIntoView( { block: "nearest", inline: "nearest" } );
|
|
561
|
+
}
|
|
562
|
+
|
|
454
563
|
return true;
|
|
455
564
|
}
|
|
456
565
|
}
|
|
@@ -497,7 +606,9 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
497
606
|
if (ev.type == 'changed' && this._selection.size ) {
|
|
498
607
|
const nsel = new Set<number>();
|
|
499
608
|
this._selection.forEach(x => {
|
|
500
|
-
|
|
609
|
+
// selection use column index, store use column id
|
|
610
|
+
const cdata = this._getCol( x & 0xfff );
|
|
611
|
+
if( cdata && this._store.hasData( x >> 12, cdata.id ) ) {
|
|
501
612
|
nsel.add( x );
|
|
502
613
|
}
|
|
503
614
|
});
|
|
@@ -857,7 +968,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
857
968
|
el.setInternalData("row", rowid);
|
|
858
969
|
el.setData("ref", mkid(rowid, col) + "");
|
|
859
970
|
|
|
860
|
-
if (this._selection.has(mkid(
|
|
971
|
+
if (this._selection.has(mkid(rowid, col))) {
|
|
861
972
|
el.addClass("selected");
|
|
862
973
|
}
|
|
863
974
|
|
|
@@ -1028,8 +1139,33 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1028
1139
|
}
|
|
1029
1140
|
|
|
1030
1141
|
|
|
1142
|
+
// MOUSEDOWN
|
|
1143
|
+
this.addDOMEvent("mousedown", (e) => {
|
|
1144
|
+
if( !this._edit || this._inEditor(e) ) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// click on another cell while editing:
|
|
1149
|
+
// validate now, rows may be rebuilt before the click event (and the click lost)
|
|
1150
|
+
const ref = targetCell(e);
|
|
1151
|
+
if( ref && ref.row!==undefined ) {
|
|
1152
|
+
this._endEdit( true, true );
|
|
1153
|
+
|
|
1154
|
+
if (!this._selection.has(ref.ref)) {
|
|
1155
|
+
this._clearSelection( false );
|
|
1156
|
+
this._addSelection(ref.ref,true);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
e.preventDefault( );
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1031
1163
|
// CLICK
|
|
1032
1164
|
this.addDOMEvent("click", (e) => {
|
|
1165
|
+
if( this._inEditor(e) ) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1033
1169
|
const ref = targetCell(e);
|
|
1034
1170
|
if (ref) {
|
|
1035
1171
|
//TODO: multiselection
|
|
@@ -1042,6 +1178,10 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1042
1178
|
|
|
1043
1179
|
// DBLCLICK
|
|
1044
1180
|
this.addDOMEvent("dblclick", (e) => {
|
|
1181
|
+
if( this._inEditor(e) ) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1045
1185
|
const ref = targetCell(e);
|
|
1046
1186
|
if (ref) {
|
|
1047
1187
|
//TODO: multiselection
|
|
@@ -1050,12 +1190,23 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1050
1190
|
this._addSelection(ref.ref,true);
|
|
1051
1191
|
}
|
|
1052
1192
|
|
|
1053
|
-
|
|
1193
|
+
const dev: EvDblClick = { context: { row: ref.row, col: ref.col } };
|
|
1194
|
+
this.fire( "dblClick", dev );
|
|
1195
|
+
|
|
1196
|
+
// checkboxes are toggled, other cells are edited
|
|
1197
|
+
if( !dev.defaultPrevented && !this._toggleCell( ref.row, ref.col ) ) {
|
|
1198
|
+
this.editCell( ref.row, ref.col );
|
|
1199
|
+
}
|
|
1054
1200
|
}
|
|
1055
1201
|
});
|
|
1056
1202
|
|
|
1057
1203
|
// CONTEXT
|
|
1058
1204
|
this.addDOMEvent("contextmenu", (e) => {
|
|
1205
|
+
// keep the native menu of the editor (copy/paste)
|
|
1206
|
+
if( this._inEditor(e) ) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1059
1210
|
const ref = targetCell(e);
|
|
1060
1211
|
if (ref) {
|
|
1061
1212
|
//TODO: multiselection
|
|
@@ -1115,6 +1266,13 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1115
1266
|
|
|
1116
1267
|
if (this._start != start || this._end != end || force) {
|
|
1117
1268
|
|
|
1269
|
+
// rows are rebuilt: the cell editor must survive
|
|
1270
|
+
const edit = this._edit;
|
|
1271
|
+
const edit_focus = edit ? edit.editor.dom.contains( document.activeElement ) : false;
|
|
1272
|
+
if( edit ) {
|
|
1273
|
+
edit.busy = true;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1118
1276
|
const rows: Component[] = [];
|
|
1119
1277
|
const headers: Component[] = [];
|
|
1120
1278
|
|
|
@@ -1175,6 +1333,10 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1175
1333
|
else {
|
|
1176
1334
|
this._vheader.addClass("@hidden");
|
|
1177
1335
|
}
|
|
1336
|
+
|
|
1337
|
+
if( edit && this._edit===edit ) {
|
|
1338
|
+
this._restoreEditor( edit, edit_focus );
|
|
1339
|
+
}
|
|
1178
1340
|
}
|
|
1179
1341
|
}
|
|
1180
1342
|
}
|
|
@@ -1211,7 +1373,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1211
1373
|
|
|
1212
1374
|
if (notify) {
|
|
1213
1375
|
const selection = this.getSelection();
|
|
1214
|
-
this.fire("selectionChange", { selection, empty: selection.length
|
|
1376
|
+
this.fire("selectionChange", { selection, empty: selection.length == 0 });
|
|
1215
1377
|
}
|
|
1216
1378
|
}
|
|
1217
1379
|
|
|
@@ -1243,5 +1405,378 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1243
1405
|
|
|
1244
1406
|
this._addSelection(mkid(row, col), true);
|
|
1245
1407
|
}
|
|
1408
|
+
|
|
1409
|
+
// :: EDITION ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* @returns the selected cell when there is exactly one
|
|
1413
|
+
*/
|
|
1414
|
+
|
|
1415
|
+
private _curCell( ): CellRef {
|
|
1416
|
+
if( this._selection.size!=1 ) {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
const ref = this._selection.values().next().value as number;
|
|
1421
|
+
return { row: ref >> 12, col: ref & 0xfff };
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
private _inEditor( e: Event ) {
|
|
1425
|
+
return !!this._edit && this._edit.editor.dom.contains( e.target as Node );
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
*
|
|
1430
|
+
*/
|
|
1431
|
+
|
|
1432
|
+
private _isEditable( row: number, col: number ) {
|
|
1433
|
+
const cdata = this._getCol( col );
|
|
1434
|
+
if( !cdata?.editable || !this._store || this.isDisabled() ) {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if( !(row>=0) || row>=this._store.getRowCount() ) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if( isFunction(cdata.editable) && !cdata.editable( row, col ) ) {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// html content & images cannot be edited by the default editor
|
|
1447
|
+
if( !cdata.editor ) {
|
|
1448
|
+
if( cdata.type=="image" || cdata.type=="icon" ) {
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if( this._store.getData( row, cdata.id ) instanceof UnsafeHtml ) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* write a value to the store (after cellChange agreement)
|
|
1462
|
+
* @returns true if the value was written
|
|
1463
|
+
*/
|
|
1464
|
+
|
|
1465
|
+
private _setCellValue( row: number, col: number, value: any ) {
|
|
1466
|
+
const cdata = this._getCol( col );
|
|
1467
|
+
if( !cdata || !this._store || row>=this._store.getRowCount() ) {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const empty = ( v: any ) => v===undefined || v===null || v==="";
|
|
1472
|
+
const same = ( a: any, b: any ) => {
|
|
1473
|
+
if( a instanceof Date && b instanceof Date ) {
|
|
1474
|
+
return a.getTime()==b.getTime();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return a===b || ( empty(a) && empty(b) );
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const oldValue = this._store.getData( row, cdata.id );
|
|
1481
|
+
if( same( value, oldValue ) ) {
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const ev: EvCellChange = { row, col, colId: cdata.id, value, oldValue };
|
|
1486
|
+
this.fire( "cellChange", ev );
|
|
1487
|
+
|
|
1488
|
+
if( ev.defaultPrevented ) {
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
this._store.setData( row, cdata.id, ev.value );
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* editable checkboxes are toggled without editor
|
|
1498
|
+
*/
|
|
1499
|
+
|
|
1500
|
+
private _toggleCell( row: number, col: number ) {
|
|
1501
|
+
const cdata = this._getCol( col );
|
|
1502
|
+
if( cdata?.type!="checkbox" || cdata.editor || !this._isEditable( row, col ) ) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
this._setCellValue( row, col, !this._store.getData( row, cdata.id ) );
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* default editor: an input matching the column type
|
|
1512
|
+
*/
|
|
1513
|
+
|
|
1514
|
+
private _createEditor( cdata: SpreadsheetColumn, value: any, clear: boolean ): Component {
|
|
1515
|
+
switch( cdata.type ) {
|
|
1516
|
+
case "number":
|
|
1517
|
+
case "money":
|
|
1518
|
+
case "percent": {
|
|
1519
|
+
const ed = new Input( { type: "number", value: !clear && isNumber(value) ? value : undefined } );
|
|
1520
|
+
ed.setAttribute( "step", "any" );
|
|
1521
|
+
return ed;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
case "date": {
|
|
1525
|
+
return new Input( { type: "date", value: !clear && value instanceof Date ? value : undefined } );
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
default: {
|
|
1529
|
+
return new Input( { type: "text", value: clear || value===undefined || value===null ? "" : value+"" } );
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* @returns the value of the editor, undefined if there is nothing to write
|
|
1536
|
+
*/
|
|
1537
|
+
|
|
1538
|
+
private _readEditor( edit: CellEdit ): any {
|
|
1539
|
+
const cdata = this._getCol( edit.col );
|
|
1540
|
+
if( !cdata ) {
|
|
1541
|
+
return undefined;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if( cdata.editor ) {
|
|
1545
|
+
const fe = edit.editor.queryInterface<IFormElement>( "form-element" );
|
|
1546
|
+
return fe && fe.isValid() ? fe.getRawValue( ) : undefined;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const input = edit.editor as Input;
|
|
1550
|
+
if( (input.dom as HTMLInputElement).validity?.badInput ) {
|
|
1551
|
+
return undefined;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const text = input.getValue( );
|
|
1555
|
+
if( text===edit.initial ) {
|
|
1556
|
+
return undefined; // untouched
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
switch( cdata.type ) {
|
|
1560
|
+
case "number":
|
|
1561
|
+
case "money":
|
|
1562
|
+
case "percent": {
|
|
1563
|
+
const v = parseFloat( text );
|
|
1564
|
+
return isNaN(v) ? null : v;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
case "date": {
|
|
1568
|
+
const [y,m,d] = text.split( "-" ).map( x => parseInt(x) );
|
|
1569
|
+
return y ? new Date( y, m-1, d ) : null;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
default: {
|
|
1573
|
+
// keep numbers as numbers
|
|
1574
|
+
const old = this._store.getData( edit.row, cdata.id );
|
|
1575
|
+
if( isNumber(old) && text!=="" && isFinite( Number(text) ) ) {
|
|
1576
|
+
return Number( text );
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
return text;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
private _focusEditor( editor: Component ) {
|
|
1585
|
+
const dom = editor.dom as HTMLElement;
|
|
1586
|
+
const el = dom.matches( "input,select,textarea" ) ? dom : dom.querySelector<HTMLElement>( "input,select,textarea,[tabindex]" );
|
|
1587
|
+
( el ?? dom ).focus( { preventScroll: true } );
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* rows have been rebuilt: put the editor back in its cell
|
|
1592
|
+
*/
|
|
1593
|
+
|
|
1594
|
+
private _restoreEditor( edit: CellEdit, focus: boolean ) {
|
|
1595
|
+
const cell = this.query( `.cell[data-ref="${mkid(edit.row,edit.col)}"]` );
|
|
1596
|
+
if( !cell ) {
|
|
1597
|
+
// the cell is no more visible
|
|
1598
|
+
edit.busy = false;
|
|
1599
|
+
this._endEdit( true, focus );
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if( edit.editor.dom.parentElement!=cell.dom ) {
|
|
1604
|
+
cell.addClass( "editing" );
|
|
1605
|
+
cell.appendContent( edit.editor );
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if( focus ) {
|
|
1609
|
+
this._focusEditor( edit.editor );
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
edit.busy = false;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
*
|
|
1617
|
+
*/
|
|
1618
|
+
|
|
1619
|
+
private _on_edit_key( ev: KeyboardEvent ) {
|
|
1620
|
+
// the grid must not handle the editor keys (navigation)
|
|
1621
|
+
ev.stopPropagation( );
|
|
1622
|
+
|
|
1623
|
+
if( ev.isComposing ) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
switch( ev.key ) {
|
|
1628
|
+
case "Enter": {
|
|
1629
|
+
this._endEdit( true, true );
|
|
1630
|
+
|
|
1631
|
+
const move = this.props.enterMove ?? "down";
|
|
1632
|
+
if( move=="down" ) {
|
|
1633
|
+
this.navigate( ev.shiftKey ? kbNav.prev : kbNav.next );
|
|
1634
|
+
}
|
|
1635
|
+
else if( move=="right" ) {
|
|
1636
|
+
this.navigate( ev.shiftKey ? kbNav.left : kbNav.right );
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
case "Tab": {
|
|
1643
|
+
this._endEdit( true, true );
|
|
1644
|
+
this.navigate( ev.shiftKey ? kbNav.left : kbNav.right );
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
case "Escape": {
|
|
1649
|
+
this._endEdit( false, true );
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
default:
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
ev.preventDefault( );
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* @param refocus - give the focus back to the grid (default: only if the editor has it)
|
|
1662
|
+
*/
|
|
1663
|
+
|
|
1664
|
+
private _endEdit( commit: boolean, refocus?: boolean ) {
|
|
1665
|
+
const edit = this._edit;
|
|
1666
|
+
if( !edit ) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
this._edit = null;
|
|
1671
|
+
|
|
1672
|
+
const value = commit ? this._readEditor( edit ) : undefined;
|
|
1673
|
+
const dom = edit.editor.dom;
|
|
1674
|
+
|
|
1675
|
+
if( refocus===undefined ) {
|
|
1676
|
+
refocus = dom.contains( document.activeElement );
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
const cell = componentFromDOM( dom.parentElement );
|
|
1680
|
+
cell?.removeClass( "editing" );
|
|
1681
|
+
dom.remove( );
|
|
1682
|
+
|
|
1683
|
+
if( refocus ) {
|
|
1684
|
+
(this.dom as HTMLElement).focus( { preventScroll: true } );
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
if( value!==undefined ) {
|
|
1688
|
+
this._setCellValue( edit.row, edit.col, value );
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* start the edition of a cell (the cell must be visible and its column editable)
|
|
1694
|
+
* @param row - row index
|
|
1695
|
+
* @param col - column index
|
|
1696
|
+
* @param clear - start with an empty editor
|
|
1697
|
+
* @returns false if the cell cannot be edited
|
|
1698
|
+
*/
|
|
1699
|
+
|
|
1700
|
+
editCell( row: number, col: number, clear = false ): boolean {
|
|
1701
|
+
this._endEdit( true );
|
|
1702
|
+
|
|
1703
|
+
const cdata = this._getCol( col );
|
|
1704
|
+
if( !this._isEditable( row, col ) || ( cdata.type=="checkbox" && !cdata.editor ) ) {
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const ref = mkid( row, col );
|
|
1709
|
+
const cell = this.query( `.cell[data-ref="${ref}"]` );
|
|
1710
|
+
if( !cell ) {
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if( this._selection.size!=1 || !this._selection.has(ref) ) {
|
|
1715
|
+
this._clearSelection( false );
|
|
1716
|
+
this._addSelection( ref, true );
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const value = this._store.getData( row, cdata.id );
|
|
1720
|
+
const editor = cdata.editor ? cdata.editor( row, col, value ) : this._createEditor( cdata, value, clear );
|
|
1721
|
+
if( !editor ) {
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const edit: CellEdit = { row, col, editor };
|
|
1726
|
+
if( !cdata.editor ) {
|
|
1727
|
+
edit.initial = (editor as Input).getValue( );
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
editor.addClass( "cell-editor" );
|
|
1731
|
+
editor.addDOMEvent( "keydown", ( ev ) => this._on_edit_key( ev ) );
|
|
1732
|
+
|
|
1733
|
+
// avoid a second dispatch of the editor handlers by the grid
|
|
1734
|
+
editor.addDOMEvent( "wheel", ( ev ) => ev.stopPropagation( ) );
|
|
1735
|
+
|
|
1736
|
+
editor.addDOMEvent( "focusout", ( ev ) => {
|
|
1737
|
+
if( this._edit!==edit || edit.busy ) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// focus is moving inside the editor
|
|
1742
|
+
const to = ev.relatedTarget as Node;
|
|
1743
|
+
if( to && editor.dom.contains(to) ) {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
this._endEdit( true, false );
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
this._edit = edit;
|
|
1751
|
+
|
|
1752
|
+
cell.addClass( "editing" );
|
|
1753
|
+
cell.appendContent( editor );
|
|
1754
|
+
this._focusEditor( editor );
|
|
1755
|
+
|
|
1756
|
+
if( !clear && !cdata.editor ) {
|
|
1757
|
+
const input = editor as Input;
|
|
1758
|
+
if( cdata.type=="number" || cdata.type=="money" || cdata.type=="percent" ) {
|
|
1759
|
+
input.selectAll( );
|
|
1760
|
+
}
|
|
1761
|
+
else if( cdata.type!="date" ) {
|
|
1762
|
+
input.select( input.getValue().length, 0 );
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* stop the current edition
|
|
1771
|
+
* @param commit - false to cancel
|
|
1772
|
+
*/
|
|
1773
|
+
|
|
1774
|
+
stopEdit( commit = true ) {
|
|
1775
|
+
this._endEdit( commit );
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
isEditing( ) {
|
|
1779
|
+
return !!this._edit;
|
|
1780
|
+
}
|
|
1246
1781
|
}
|
|
1247
1782
|
|