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.
@@ -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
- default:
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&0xff; }
395
- if( M===undefined || M<row ) { M = row; col=x&0xff; }
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
- //this._scrollToIndex( nline );
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
- if( this._store.hasData( x ) ) {
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(col, rowid))) {
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
- this.fire( "dblClick", { context: { row: ref.row, col: ref.col } } );
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 != 0 });
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