canvas-datagrid 0.3.20 → 0.3.24

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/dist/types.d.ts CHANGED
@@ -505,6 +505,11 @@ declare class canvasDatagrid {
505
505
  * @param offsetY - The number of rows to offset the selection.
506
506
  */
507
507
  static moveSelection(offsetX: number, offsetY: number): void;
508
+ /**
509
+ * Deletes currently selected data.
510
+ * @param dontDraw - Suppress the draw method after the selection change.
511
+ */
512
+ static deleteSelectedData(dontDraw: boolean): void;
508
513
  /**
509
514
  * Moves data in the provided selection to another position in the grid. Moving data off the edge of the schema (columns/x) will truncate data.
510
515
  * @param sel - 2D array representing selected rows and columns. `canvasDatagrid.selections` is in this format and can be used here.
package/lib/button.js CHANGED
@@ -150,6 +150,7 @@ export default function (self) {
150
150
  if (item.click) {
151
151
  menuItemContainer.addEventListener('click', function (ev) {
152
152
  item.click.apply(self, [ev]);
153
+ self.disposeButton();
153
154
  });
154
155
  }
155
156
  }
package/lib/docs.js CHANGED
@@ -460,6 +460,14 @@
460
460
  * @param {object} e.boundRowIndexMap Mapping of view data row to bound data row index.
461
461
  * @param {object} e.boundColumnIndexMap Mapping of view column order to bound column order.
462
462
  */
463
+ /**
464
+ * Fires when the selected is deleted by pressing backspace or delete key.
465
+ * @event
466
+ * @name canvasDatagrid#afterdelete
467
+ * @param {object} e Event object.
468
+ * @param {object} e.ctx Canvas context.
469
+ * @param {array} e.cells Cells affected by the delete action. Each item in the array is a tuple of [rowIndex, columnIndex, boundRowIndex, boundColumnIndex].
470
+ */
463
471
  /**
464
472
  * Fires when the user clicks on the "drill in" arrow. When the arrow is clicked a new
465
473
  * grid is created and nested inside of the row. The grid, the row data and row index are passed
@@ -552,7 +560,7 @@
552
560
  * @name canvasDatagrid#afterpaste
553
561
  * @param {object} e Event object
554
562
  * @param {object} e.ctx Canvas context.
555
- * @param {array} e.cells Cells affected by the paste action. Each item in the array is a tuple of [rowIndex, columnIndex].
563
+ * @param {array} e.cells Cells affected by the paste action. Each item in the array is a tuple of [rowIndex, columnIndex, boundRowIndex, boundColumnIndex].
556
564
  */
557
565
  /**
558
566
  * Fired just before a cell is drawn onto the canvas. `e.preventDefault();` prevents the cell from being drawn.
@@ -472,10 +472,18 @@ export default function (self) {
472
472
  };
473
473
  if (
474
474
  cell.isFilterable &&
475
- x > cell.x + cell.width - self.style.filterButtonWidth &&
476
- x < cell.x + cell.width &&
477
- y > cell.y + cell.height - self.style.filterButtonHeight &&
478
- y < cell.y + cell.height
475
+ x >
476
+ cell.x +
477
+ cell.width +
478
+ self.canvasOffsetLeft -
479
+ self.style.filterButtonWidth &&
480
+ x < cell.x + cell.width + self.canvasOffsetLeft &&
481
+ y >
482
+ cell.y +
483
+ cell.height +
484
+ self.canvasOffsetTop -
485
+ self.style.filterButtonHeight &&
486
+ y < cell.y + cell.height + self.canvasOffsetTop
479
487
  ) {
480
488
  self.hovers.onFilterButton = true;
481
489
  self.draw();
@@ -789,7 +797,26 @@ export default function (self) {
789
797
  }
790
798
  self.ellipsisCache = {};
791
799
  };
792
- self.stopDragResize = function () {
800
+ self.stopDragResize = function (event) {
801
+ const pos = self.getLayerPos(event);
802
+
803
+ if (self.dragMode === 'ew-resize') {
804
+ const hasMoved = !!(pos.x - self.dragStart.x);
805
+ // Check that dragItem is selected or part of selection.
806
+ const dragItemIsSelected = self.isColumnSelected(
807
+ self.dragItem.columnIndex,
808
+ );
809
+
810
+ if (hasMoved && dragItemIsSelected) {
811
+ const width = Math.max(
812
+ self.resizingStartingWidth + pos.x - self.dragStart.x,
813
+ self.style.minColumnWidth,
814
+ );
815
+ // If the column is selected, resize it to width plus any other selected columns.
816
+ self.fitSelectedColumns(width);
817
+ }
818
+ }
819
+
793
820
  self.resize();
794
821
  document.body.removeEventListener(
795
822
  'mousemove',
@@ -1420,6 +1447,8 @@ export default function (self) {
1420
1447
  self.selectNone();
1421
1448
  } else if (ctrl && e.key === 'a') {
1422
1449
  self.selectAll();
1450
+ } else if (['Backspace', 'Delete'].includes(e.key)) {
1451
+ self.deleteSelectedData();
1423
1452
  } else if (e.key === 'ArrowDown') {
1424
1453
  y += 1;
1425
1454
  } else if (e.key === 'ArrowUp') {
@@ -1599,7 +1628,17 @@ export default function (self) {
1599
1628
  self.currentCell.context === 'ew-resize' &&
1600
1629
  self.currentCell.style === 'columnHeaderCell'
1601
1630
  ) {
1602
- self.fitColumnToValues(self.currentCell.header.name);
1631
+ // Check that double-clicked cell is selected or part of selection.
1632
+ const currentCellIsSelected = self.isColumnSelected(
1633
+ self.currentCell.columnIndex,
1634
+ );
1635
+
1636
+ if (currentCellIsSelected) {
1637
+ // There might be more
1638
+ self.fitSelectedColumns();
1639
+ } else {
1640
+ self.fitColumnToValues(self.currentCell.header.name);
1641
+ }
1603
1642
  } else if (
1604
1643
  self.currentCell.context === 'ew-resize' &&
1605
1644
  self.currentCell.style === 'cornerCell'
@@ -1831,105 +1870,40 @@ export default function (self) {
1831
1870
  self.draw();
1832
1871
  });
1833
1872
  };
1834
- self.cut = function (e) {
1835
- self.copy(e);
1836
- const schema = self.getSchema();
1873
+ self.cut = function (event) {
1874
+ if (self.dispatchEvent('cut', { NativeEvent: event })) {
1875
+ return;
1876
+ }
1837
1877
 
1838
- var affectedCells = [];
1839
- for (const [rowIndex, row] of self.selections.entries()) {
1840
- if (!row) continue;
1878
+ // Expecting instance of `ClipboardEvent` with `clipboardData` attribute
1879
+ if (!self.hasFocus || !event.clipboardData) {
1880
+ return;
1881
+ }
1841
1882
 
1842
- const boundRowIndex = self.getBoundRowIndexFromViewRowIndex(rowIndex);
1883
+ self.copySelectedCellsToClipboard(event.clipboardData);
1843
1884
 
1844
- for (const columnIndex of row) {
1845
- const boundColumnIndex = self.getBoundColumnIndexFromViewColumnIndex(
1846
- columnIndex,
1847
- );
1848
- const colName = schema[boundColumnIndex].name;
1849
-
1850
- self.viewData[rowIndex][colName] = '';
1885
+ const affectedCells = self.clearSelectedCells();
1851
1886
 
1852
- affectedCells.push([
1853
- rowIndex,
1854
- columnIndex,
1855
- boundRowIndex,
1856
- boundColumnIndex,
1857
- ]);
1858
- }
1859
- }
1887
+ self.dispatchEvent('aftercut', {
1888
+ cells: affectedCells,
1889
+ });
1860
1890
 
1861
- if (self.dispatchEvent('cut', { NativeEvent: e, cells: affectedCells })) {
1862
- return;
1863
- }
1891
+ requestAnimationFrame(() => self.draw());
1892
+ event.preventDefault();
1864
1893
  };
1865
- self.copy = function (e) {
1866
- if (self.dispatchEvent('copy', { NativeEvent: e })) {
1894
+ self.copy = function (event) {
1895
+ if (self.dispatchEvent('copy', { NativeEvent: event })) {
1867
1896
  return;
1868
1897
  }
1869
- if (!self.hasFocus || !e.clipboardData) {
1898
+
1899
+ // Expecting instance of `ClipboardEvent` with `clipboardData` attribute
1900
+ if (!self.hasFocus || !event.clipboardData) {
1870
1901
  return;
1871
1902
  }
1872
- var t = '',
1873
- d = '',
1874
- textRows = [],
1875
- sData = self.getSelectedData(),
1876
- s = self.getSchema(),
1877
- sSorted = [],
1878
- firstRowKeys,
1879
- isNeat = true; // Selected like [[0, 1], [0, 1]] of [[0, 3]] is neat; Selected like [[0, 1], [1, 2]] is untidy
1880
- function htmlSafe(v) {
1881
- if (typeof v === 'number') return v;
1882
- return v.replace(/</g, '&lt;').replace(/>/g, '&gt;');
1883
- }
1884
- s.forEach(function (column, columnIndex) {
1885
- sSorted.push(s[self.orders.columns[columnIndex]]);
1886
- });
1887
- if (sData.length > 0) {
1888
- sData.forEach((row) => {
1889
- if (!row) return;
1890
- var rowKeys = Object.keys(row);
1891
- var textRow = [];
1892
- if (!firstRowKeys) firstRowKeys = Object.keys(row);
1893
- if (isNeat && rowKeys.length !== firstRowKeys.length) isNeat = false;
1894
- sSorted.forEach(function (column, columnIndex) {
1895
- if (rowKeys.indexOf(String(column.name)) < 0) {
1896
- if (firstRowKeys.indexOf(String(column.name)) < 0) {
1897
- return;
1898
- } else if (isNeat) {
1899
- isNeat = false;
1900
- }
1901
- }
1902
- textRow.push(row[column.name] || '');
1903
- });
1904
- textRows.push(textRow);
1905
- });
1906
- if (isNeat) {
1907
- t = textRows.map((row) => row.join('\t')).join('\n');
1908
- /**
1909
- * The html content copied by Excel has not header
1910
- */
1911
- d += '<table>';
1912
- d += textRows
1913
- .map(
1914
- (row) =>
1915
- '<tr>' +
1916
- row.map((value) => '<td>' + htmlSafe(value) + '</td>').join('') +
1917
- '</tr>',
1918
- )
1919
- .join('');
1920
- d += '</table>';
1921
- } else {
1922
- t = textRows.map((row) => row.join('')).join('');
1923
- d = t;
1924
- }
1925
- if (t) {
1926
- e.clipboardData.setData('text/html', d);
1927
- e.clipboardData.setData('text/plain', t);
1928
- e.clipboardData.setData('text/csv', t);
1929
- e.clipboardData.setData('application/json', JSON.stringify(sData));
1930
- }
1931
- e.preventDefault();
1932
- }
1903
+
1904
+ self.copySelectedCellsToClipboard(event.clipboardData);
1905
+
1906
+ event.preventDefault();
1933
1907
  };
1934
1908
  return;
1935
1909
  }
@@ -86,11 +86,49 @@ const parseData = function (data, mimeType) {
86
86
  return parseText(data);
87
87
  };
88
88
 
89
+ const htmlSafe = function (value) {
90
+ if (typeof value === 'number') return value;
91
+ return value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
92
+ };
93
+
94
+ const createTextString = function (selectedData, isNeat) {
95
+ // Selected like [[0, 1], [0, 1]] of [[0, 3]] is neat; Selected like [[0, 1], [1, 2]] is untidy.
96
+ // If not isNeat we just return a simple string of concatenated values.
97
+ if (!isNeat)
98
+ return selectedData.map((row) => Object.values(row).join('')).join('');
99
+
100
+ // If isNeat, we can create tab separated mutti-line text.
101
+ return selectedData.map((row) => Object.values(row).join('\t')).join('\n');
102
+ };
103
+
104
+ const createHTMLString = function (selectedData, isNeat) {
105
+ if (!isNeat) return createTextString(selectedData, isNeat);
106
+
107
+ // If isNeat, we can create a HTML table with the selected data.
108
+ let htmlString = '<table>';
109
+ htmlString += selectedData
110
+ .map(
111
+ (row) =>
112
+ '<tr>' +
113
+ Object.values(row)
114
+ .map((value) => '<td>' + htmlSafe(value) + '</td>')
115
+ .join('') +
116
+ '</tr>',
117
+ )
118
+ .join('');
119
+ htmlString += '</table>';
120
+
121
+ return htmlString;
122
+ };
123
+
89
124
  export {
125
+ createTextString,
126
+ createHTMLString,
90
127
  isSupportedHtml,
91
- sanitizeElementData,
128
+ htmlSafe,
92
129
  parseData,
93
130
  parseHtmlTable,
94
131
  parseHtmlText,
95
132
  parseText,
133
+ sanitizeElementData,
96
134
  };
package/lib/intf.js CHANGED
@@ -2,6 +2,8 @@
2
2
  /*globals HTMLElement: false, Reflect: false, define: true, MutationObserver: false, requestAnimationFrame: false, performance: false, btoa: false*/
3
3
  'use strict';
4
4
 
5
+ import { createHTMLString, createTextString } from './events/util';
6
+
5
7
  export default function (self, ctor) {
6
8
  self.scale = 1;
7
9
  self.orders = {
@@ -242,6 +244,89 @@ export default function (self, ctor) {
242
244
 
243
245
  return selectedCells;
244
246
  };
247
+ self.copySelectedCellsToClipboard = function (clipboardData) {
248
+ const selectedData = [];
249
+ const schema = self.getSchema();
250
+ let firstRowAsString;
251
+ let isNeat = true; // Selected like [[0, 1], [0, 1]] of [[0, 3]] is neat; Selected like [[0, 1], [1, 2]] is untidy
252
+
253
+ for (const [rowIndex, row] of self.selections.entries()) {
254
+ // If no cells are selected for a particular rowIndex the selections array will contain an empty element for that rowIndex.
255
+ if (!row) continue;
256
+
257
+ // Convert to string for easy comparison to the first row.
258
+ const rowAsString = row.join(',').toString();
259
+ const rowDict = {};
260
+
261
+ if (!firstRowAsString) firstRowAsString = row.join(',').toString();
262
+ if (isNeat && rowAsString !== firstRowAsString) isNeat = false;
263
+
264
+ const boundRowIndex = self.getBoundRowIndexFromViewRowIndex(rowIndex);
265
+
266
+ for (let columnIndex of row) {
267
+ // If the whole row is selected the columnIndex for the rowHeader is -1.
268
+ if (columnIndex < 0) continue;
269
+
270
+ const boundColumnIndex = self.getBoundColumnIndexFromViewColumnIndex(
271
+ columnIndex,
272
+ );
273
+ const columnName = schema[boundColumnIndex].name;
274
+
275
+ const value = self.originalData[boundRowIndex][columnName];
276
+
277
+ rowDict[columnName] = value;
278
+ }
279
+ selectedData.push(rowDict);
280
+ }
281
+
282
+ if (selectedData.length > 0) {
283
+ const textString = createTextString(selectedData, isNeat);
284
+ const htmlString = createHTMLString(selectedData, isNeat);
285
+
286
+ const copiedData = {
287
+ 'text/plain': textString,
288
+ 'text/html': htmlString,
289
+ 'text/csv': textString,
290
+ 'application/json': JSON.stringify(selectedData),
291
+ };
292
+
293
+ for (const [mimeType, data] of Object.entries(copiedData)) {
294
+ clipboardData.setData(mimeType, data);
295
+ }
296
+ }
297
+ };
298
+ self.clearSelectedCells = function () {
299
+ const schema = self.getSchema();
300
+ const affectedCells = [];
301
+
302
+ for (const [rowIndex, row] of self.selections.entries()) {
303
+ // If no cells are selected for a particular rowIndex the selections array will contain an empty element for that rowIndex.
304
+ if (!row) continue;
305
+
306
+ const boundRowIndex = self.getBoundRowIndexFromViewRowIndex(rowIndex);
307
+
308
+ for (let columnIndex of row) {
309
+ // If the whole row is selected the columnIndex for the rowHeader is -1.
310
+ if (columnIndex < 0) continue;
311
+
312
+ const boundColumnIndex = self.getBoundColumnIndexFromViewColumnIndex(
313
+ columnIndex,
314
+ );
315
+ const columnName = schema[boundColumnIndex].name;
316
+
317
+ self.viewData[boundRowIndex][columnName] = '';
318
+
319
+ affectedCells.push([
320
+ rowIndex,
321
+ columnIndex,
322
+ boundRowIndex,
323
+ boundColumnIndex,
324
+ ]);
325
+ }
326
+ }
327
+
328
+ return affectedCells;
329
+ };
245
330
  self.getBoundRowIndexFromViewRowIndex = function (viewRowIndex) {
246
331
  if (self.boundRowIndexMap && self.boundRowIndexMap.has(viewRowIndex)) {
247
332
  return self.boundRowIndexMap.get(viewRowIndex);
@@ -311,6 +396,27 @@ export default function (self, ctor) {
311
396
  self.createRowOrders = function () {
312
397
  self.orders.rows = fillArray(0, self.originalData.length - 1);
313
398
  };
399
+ self.fitSelectedColumns = function (width) {
400
+ const selectedColumns = self.selections[0];
401
+ const schema = self.getSchema();
402
+
403
+ for (const selectedColumn of selectedColumns) {
404
+ // Make sure the column is not the row header and that the whole column has in fact been selected.
405
+ if (selectedColumn >= 0 && self.isColumnSelected(selectedColumn)) {
406
+ if (isNaN(width)) {
407
+ const column = schema[self.orders.columns[selectedColumn]];
408
+ self.fitColumnToValues(column.name);
409
+ } else {
410
+ self.sizes.columns[selectedColumn] = width;
411
+ self.dispatchEvent('resizecolumn', {
412
+ x: width,
413
+ y: self.resizingStartingHeight,
414
+ draggingItem: self.currentCell,
415
+ });
416
+ }
417
+ }
418
+ }
419
+ };
314
420
  self.getVisibleSchema = function () {
315
421
  return self.getSchema().filter(function (col) {
316
422
  return !col.hidden;
@@ -557,6 +663,7 @@ export default function (self, ctor) {
557
663
  self.controlInput.removeEventListener('keypress', self.keypress, false);
558
664
  self.controlInput.removeEventListener('keyup', self.keyup, false);
559
665
  self.controlInput.removeEventListener('keydown', self.keydown, false);
666
+ document.body.removeEventListener('mousemove', self.mousemove);
560
667
  window.removeEventListener('resize', self.resize);
561
668
  if (self.observer && self.observer.disconnect) {
562
669
  self.observer.disconnect();
@@ -734,6 +841,7 @@ export default function (self, ctor) {
734
841
  self.pointerLockPosition = { x: 0, y: 0 };
735
842
  Object.keys(self.style).forEach(self.parseStyleValue);
736
843
  self.intf.moveSelection = self.moveSelection;
844
+ self.intf.deleteSelectedData = self.deleteSelectedData;
737
845
  self.intf.moveTo = self.moveTo;
738
846
  self.intf.addEventListener = self.addEventListener;
739
847
  self.intf.removeEventListener = self.removeEventListener;
@@ -734,9 +734,23 @@ export default function (self) {
734
734
  if (!self.canvas) {
735
735
  return;
736
736
  }
737
- self.sizes.columns[
738
- name === 'cornerCell' ? -1 : self.getHeaderByName(name).index
739
- ] = Math.max(self.findColumnMaxTextLength(name), self.style.minColumnWidth);
737
+
738
+ const columnIndex =
739
+ name === 'cornerCell' ? -1 : self.getHeaderByName(name).index;
740
+
741
+ const newSize = Math.max(
742
+ self.findColumnMaxTextLength(name),
743
+ self.style.minColumnWidth,
744
+ );
745
+
746
+ self.sizes.columns[columnIndex] = newSize;
747
+
748
+ self.dispatchEvent('resizecolumn', {
749
+ x: newSize,
750
+ y: self.resizingStartingHeight,
751
+ draggingItem: self.currentCell,
752
+ });
753
+
740
754
  if (!internal) {
741
755
  self.resize();
742
756
  self.draw(true);
@@ -856,6 +870,26 @@ export default function (self) {
856
870
  });
857
871
  self.selections = sel;
858
872
  };
873
+ /**
874
+ * Deletes currently selected data.
875
+ * @memberof canvasDatagrid
876
+ * @name deleteSelectedData
877
+ * @method
878
+ * @param {boolean} dontDraw Suppress the draw method after the selection change.
879
+ */
880
+ self.deleteSelectedData = function (dontDraw) {
881
+ const affectedCells = self.clearSelectedCells();
882
+
883
+ self.dispatchEvent('afterdelete', {
884
+ cells: affectedCells,
885
+ });
886
+
887
+ if (dontDraw) {
888
+ return;
889
+ }
890
+
891
+ requestAnimationFrame(() => self.draw());
892
+ };
859
893
  /**
860
894
  * Moves data in the provided selection to another position in the grid. Moving data off the edge of the schema (columns/x) will truncate data.
861
895
  * @memberof canvasDatagrid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-datagrid",
3
- "version": "0.3.20",
3
+ "version": "0.3.24",
4
4
  "description": "Canvas based data grid web component. Capable of displaying millions of contiguous hierarchical rows and columns without paging or loading, on a single canvas element.",
5
5
  "main": "./dist/canvas-datagrid.js",
6
6
  "module": "./dist/canvas-datagrid.module.js",