diff --git a/docs/TOC.md b/docs/TOC.md
index add70b771..48e4ff5e0 100644
--- a/docs/TOC.md
+++ b/docs/TOC.md
@@ -44,6 +44,7 @@
* [Custom Tooltip](grid-functionalities/custom-tooltip.md)
* [Context Menu](grid-functionalities/Context-Menu.md)
* [Custom Footer](grid-functionalities/Custom-Footer.md)
+* [Excel Copy Buffer Plugin](grid-functionalities/excel-copy-buffer.md)
* [Export to Excel](grid-functionalities/Export-to-Excel.md)
* [Export to File (csv/txt)](grid-functionalities/Export-to-Text-File.md)
* [Grid Menu](grid-functionalities/Grid-Menu.md)
diff --git a/docs/events/Available-Events.md b/docs/events/Available-Events.md
index 46ff4cabe..41d5b1ba8 100644
--- a/docs/events/Available-Events.md
+++ b/docs/events/Available-Events.md
@@ -38,6 +38,7 @@ handleOnHeaderMenuCommand(e) {
- `onCopyCells`
- `onCopyCancelled`
- `onPasteCells`
+ - `onBeforePasteCell`
#### SlickContextMenu (extension)
- `onContextMenuClearGrouping`
diff --git a/docs/grid-functionalities/excel-copy-buffer.md b/docs/grid-functionalities/excel-copy-buffer.md
new file mode 100644
index 000000000..ca03ea688
--- /dev/null
+++ b/docs/grid-functionalities/excel-copy-buffer.md
@@ -0,0 +1,129 @@
+### Description
+Just like Excel you can select multiple cell and copy (`Ctrl+C`) and paste to Excel (`Ctrl+V`). However what you must know is that this plugin evaluate every single cell by their values (the raw value unless you specify otherwise, continue reading for more info).
+
+### Usage
+All you need to do is enable the Grid Option `enableExcelCopyBuffer: true` and give it a try. From your grid, start selecting multiple cells with the mouse then copy (with `Ctrl+C`) and paste to Excel (with `Ctrl+V`)
+
+##### Component
+```typescript
+this.columnDefinitions = [
+ { id: 'title', name: 'Title', field: 'id' },
+ { id: 'description', name: 'Description', field: 'description' },
+ { id: 'duration', name: 'Duration (days)', field: 'duration', type: FieldType.number },
+];
+this.gridOptions = {
+ enableExcelCopyBuffer: true,
+};
+```
+
+### Copy & Paste with Cell Formatter
+What if you have a date in UTC format in your dataset but your grid shows it as a Date ISO format? In that case, you are using a Formatter (e.g. `formatter: Formatters.dateIso`) and you wish to use that formatter. Good news, that is supported with and to make is simpler for the implementation, we will use a flag that already exist which is `exportWithFormatter` and is used by the `Export to File` service (for more info, read [Wiki - Export to File](Export-to-Text-File.md)
+
+The `exportWithFormatter` can be used in 2 ways, on each column definition independently or for the entire grid through it's grid option.
+##### `exportWithFormatter` through each Column Definition
+```typescript
+this.columnDefinitions = [
+ {
+ id: 'start', name: 'Start', field: 'start',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true
+ },
+ {
+ id: 'finish', name: 'Finish', field: 'finish',
+ formatter: Formatters.dateIso,
+ exportWithFormatter: true
+ },
+];
+
+this.gridOptions = {
+ enableExcelCopyBuffer: true,
+};
+```
+
+##### `exportWithFormatter` through Grid Options
+```typescript
+this.columnDefinitions = [
+ { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso },
+ { id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso },
+];
+
+this.gridOptions = {
+ enableExcelCopyBuffer: true,
+ exportOptions: {
+ // set at the grid option level, meaning all column will evaluate the Formatter (when it has a Formatter defined)
+ exportWithFormatter: true
+ },
+};
+```
+#### Sanitize Formatter Ouput
+In some cases a Formatter can be formed of HTML and that will end up showing in your Copy+Paste. You can simply use the `sanitizeDataExport` flag which will remove any HTML tags from the output. For an example below, let say that our first Title column are all displayed in bold in the grid (e.g. `Title 1 `), we want to sanitize that output a regular text output (e.g. `Title 1`)
+
+##### `exportWithFormatter` through each Column Definition
+```typescript
+this.columnDefinitions = [
+ {
+ id: 'title', name: 'Title', field: 'id',
+ formatter: Formatters.bold,
+ exportWithFormatter: true,
+ sanitizeDataExport: true
+ }
+];
+
+this.gridOptions = {
+ enableExcelCopyBuffer: true
+};
+```
+
+##### `exportWithFormatter` through Grid Options
+```typescript
+this.columnDefinitions = [
+ { id: 'title', name: 'Title', field: 'id', formatter: Formatters.bold }
+];
+
+this.gridOptions = {
+ enableExcelCopyBuffer: true,
+ exportOptions: {
+ exportWithFormatter: true,
+ sanitizeDataExport: true
+ },
+};
+```
+
+### Disable pasting on specific columns
+If you want to disable pasting values for specific columns you can deactivate it using the denyPaste property on the Column config.
+
+```typescript
+this.columnDefinitions = [
+ {
+ id: 'colA', name: 'Col A', field: 'col_a',
+ formatter: Formatters.bold,
+ exportWithFormatter: true,
+ sanitizeDataExport: true,
+ denyPaste: true // <------------
+ }
+];
+```
+
+This will even work in situations where your table copy buffer stretches over disabled cells, by simply skipping them. So for the following config (x = paste disabled; o = paste enabled), pasting a 3 cell stretching table buffer will result in Col A and C being updated but ColB ignoring the paste and keeping its original value
+
+Col A | Col B | Col C \
+\---------------------\
+ o | x | o \
+NEW | x | NEW
+
+### Disable pasting on specific cells
+If you need even more fine grained control, you can make use of the gridOption `onBeforePasteCell`:
+
+```typescript
+this.gridOptions = {
+ enableExcelCopyBuffer: true,
+ excelCopyBufferOptions: {
+ onBeforePasteCell: (e, args) => {
+ // e.g deny paste on first cell
+ return args.cell > 0;
+ }
+ }
+};
+```
+
+This way you have full control, via the args parameters, to get a ref to the current row and cell being updated. Also keep in mind that while performing a buffered table paste (e.g three cols at once) and only one of them has a denying condition, the other cells paste will pass successfully.
\ No newline at end of file
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example11-modal.ts b/examples/vite-demo-vanilla-bundle/src/examples/example11-modal.ts
index 9de2eb8b7..677ebbb97 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example11-modal.ts
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example11-modal.ts
@@ -1,7 +1,6 @@
import {
type Column,
type DOMEvent,
- emptyElement,
type Formatter,
Formatters,
type GridOption,
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example19.html b/examples/vite-demo-vanilla-bundle/src/examples/example19.html
index 59a910709..95581f7d2 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example19.html
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example19.html
@@ -1,3 +1,10 @@
+
Example 19 - ExcelCopyBuffer with Cell Selection
(with Salesforce Theme)
@@ -11,7 +18,8 @@
- Grid - using enableExcelCopyBuffer
which uses SlickCellSelectionModel
+ Grid - using enableExcelCopyBuffer
which uses SlickCellSelectionModel
+ The complete first row and the cells C - E of the second row are not allowing to paste values.
console.log('onCopyCells', args.ranges),
// onPasteCells: (e, args: { ranges: SelectedRange[] }) => console.log('onPasteCells', args.ranges),
// onCopyCancelled: (e, args: { ranges: SelectedRange[] }) => console.log('onCopyCancelled', args.ranges),
- // }
+ onBeforePasteCell: (_e, args) => {
+ // deny the whole first row and the cells C-E of the second row
+ return !(args.row === 0 || (args.row === 1 && args.cell > 2 && args.cell < 6));
+ }
+ }
};
}
diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts
index 7a5736bbf..5cbd255e1 100644
--- a/packages/common/src/core/slickGrid.ts
+++ b/packages/common/src/core/slickGrid.ts
@@ -1329,7 +1329,7 @@ export class SlickGrid = Column, O e
this.columns[idx].toolTip = toolTip;
}
- this.trigger(this.onBeforeHeaderCellDestroy, {
+ this.triggerEvent(this.onBeforeHeaderCellDestroy, {
node: header,
column: columnDef,
grid: this
@@ -1340,7 +1340,7 @@ export class SlickGrid = Column, O e
this.applyHtmlCode(header.children[0], title);
}
- this.trigger(this.onHeaderCellRendered, {
+ this.triggerEvent(this.onHeaderCellRendered, {
node: header,
column: columnDef,
grid: this
@@ -1447,7 +1447,7 @@ export class SlickGrid = Column, O e
const columnElements = footer.querySelectorAll('.slick-footerrow-column');
columnElements.forEach((column) => {
const columnDef = Utils.storage.get(column, 'column');
- this.trigger(this.onBeforeFooterRowCellDestroy, {
+ this.triggerEvent(this.onBeforeFooterRowCellDestroy, {
node: column,
column: columnDef,
grid: this
@@ -1470,7 +1470,7 @@ export class SlickGrid = Column, O e
Utils.storage.put(footerRowCell, 'column', m);
- this.trigger(this.onFooterRowCellRendered, {
+ this.triggerEvent(this.onFooterRowCellRendered, {
node: footerRowCell,
column: m,
grid: this
@@ -1493,7 +1493,7 @@ export class SlickGrid = Column, O e
columnElements.forEach((column) => {
const columnDef = Utils.storage.get(column, 'column');
if (columnDef) {
- this.trigger(this.onBeforeHeaderCellDestroy, {
+ this.triggerEvent(this.onBeforeHeaderCellDestroy, {
node: column,
column: columnDef,
grid: this
@@ -1515,7 +1515,7 @@ export class SlickGrid = Column, O e
columnElements.forEach((column) => {
const columnDef = Utils.storage.get(column, 'column');
if (columnDef) {
- this.trigger(this.onBeforeHeaderRowCellDestroy, {
+ this.triggerEvent(this.onBeforeHeaderRowCellDestroy, {
node: this,
column: columnDef,
grid: this
@@ -1532,7 +1532,7 @@ export class SlickGrid = Column, O e
footerRowLColumnElements.forEach((column) => {
const columnDef = Utils.storage.get(column, 'column');
if (columnDef) {
- this.trigger(this.onBeforeFooterRowCellDestroy, {
+ this.triggerEvent(this.onBeforeFooterRowCellDestroy, {
node: this,
column: columnDef,
grid: this
@@ -1546,7 +1546,7 @@ export class SlickGrid = Column, O e
footerRowRColumnElements.forEach((column) => {
const columnDef = Utils.storage.get(column, 'column');
if (columnDef) {
- this.trigger(this.onBeforeFooterRowCellDestroy, {
+ this.triggerEvent(this.onBeforeFooterRowCellDestroy, {
node: this,
column: columnDef,
grid: this
@@ -1603,7 +1603,7 @@ export class SlickGrid = Column, O e
}
}
- this.trigger(this.onHeaderCellRendered, {
+ this.triggerEvent(this.onHeaderCellRendered, {
node: header,
column: m,
grid: this
@@ -1621,7 +1621,7 @@ export class SlickGrid = Column, O e
Utils.storage.put(headerRowCell, 'column', m);
- this.trigger(this.onHeaderRowCellRendered, {
+ this.triggerEvent(this.onHeaderRowCellRendered, {
node: headerRowCell,
column: m,
grid: this
@@ -1632,7 +1632,7 @@ export class SlickGrid = Column, O e
const footerRowCell = createDomElement('div', { className: `ui-state-default slick-state-default slick-footerrow-column l${i} r${i}` }, footerRowTarget);
Utils.storage.put(footerRowCell, 'column', m);
- this.trigger(this.onFooterRowCellRendered, {
+ this.triggerEvent(this.onFooterRowCellRendered, {
node: footerRowCell,
column: m,
grid: this
@@ -1644,7 +1644,7 @@ export class SlickGrid = Column, O e
this.setupColumnResize();
if (this._options.enableColumnReorder) {
if (typeof this._options.enableColumnReorder === 'function') {
- this._options.enableColumnReorder(this as unknown as SlickGrid, this._headers, this.headerColumnWidthDiff, this.setColumns as any, this.setupColumnResize, this.columns, this.getColumnIndex, this.uid, this.trigger);
+ this._options.enableColumnReorder(this as unknown as SlickGrid, this._headers, this.headerColumnWidthDiff, this.setColumns as any, this.setupColumnResize, this.columns, this.getColumnIndex, this.uid, this.triggerEvent);
} else {
this.setupColumnReorder();
}
@@ -1735,9 +1735,9 @@ export class SlickGrid = Column, O e
};
}
- if (this.trigger(this.onBeforeSort, onSortArgs, e).getReturnValue() !== false) {
+ if (this.triggerEvent(this.onBeforeSort, onSortArgs, e).getReturnValue() !== false) {
this.setSortColumns(this.sortColumns);
- this.trigger(this.onSort, onSortArgs, e);
+ this.triggerEvent(this.onSort, onSortArgs, e);
}
}
});
@@ -1820,7 +1820,7 @@ export class SlickGrid = Column, O e
}
this.setColumns(reorderedColumns);
- this.trigger(this.onColumnsReordered, { impactedColumns: this.getImpactedColumns(limit) });
+ this.triggerEvent(this.onColumnsReordered, { impactedColumns: this.getImpactedColumns(limit) });
e.stopPropagation();
this.setupColumnResize();
if (this.activeCellNode) {
@@ -1855,7 +1855,7 @@ export class SlickGrid = Column, O e
protected handleResizeableHandleDoubleClick(evt: MouseEvent & { target: HTMLDivElement; }) {
const triggeredByColumn = evt.target.parentElement!.id.replace(this.uid, '');
- this.trigger(this.onColumnsResizeDblClick, { triggeredByColumn });
+ this.triggerEvent(this.onColumnsResizeDblClick, { triggeredByColumn });
}
protected setupColumnResize() {
@@ -2154,7 +2154,7 @@ export class SlickGrid = Column, O e
if (this._options.syncColumnCellResize) {
this.applyColumnWidths();
}
- this.trigger(this.onColumnsDrag, {
+ this.triggerEvent(this.onColumnsDrag, {
triggeredByColumn: resizeElms.resizeableElement,
resizeHandle: resizeElms.resizeableHandleElement
});
@@ -2163,7 +2163,7 @@ export class SlickGrid = Column, O e
resizeElms.resizeableElement.classList.remove('slick-header-column-active');
const triggeredByColumn = resizeElms.resizeableElement.id.replace(this.uid, '');
- if (this.trigger(this.onBeforeColumnsResize, { triggeredByColumn }).getReturnValue() === true) {
+ if (this.triggerEvent(this.onBeforeColumnsResize, { triggeredByColumn }).getReturnValue() === true) {
this.applyColumnHeaderWidths();
}
let newWidth;
@@ -2178,7 +2178,7 @@ export class SlickGrid = Column, O e
}
this.updateCanvasWidth(true);
this.render();
- this.trigger(this.onColumnsResized, { triggeredByColumn });
+ this.triggerEvent(this.onColumnsResized, { triggeredByColumn });
clearTimeout(this._columnResizeTimer);
this._columnResizeTimer = setTimeout(() => { this.columnResizeDragging = false; }, 300);
}
@@ -2452,7 +2452,7 @@ export class SlickGrid = Column, O e
this.slickResizableInstances = this.destroyAllInstances(this.slickResizableInstances) as InteractionBase[];
this.getEditorLock()?.cancelCurrentEdit();
- this.trigger(this.onBeforeDestroy, {});
+ this.triggerEvent(this.onBeforeDestroy, {});
let i = this.plugins.length;
while (i--) {
@@ -2719,7 +2719,7 @@ export class SlickGrid = Column, O e
this.applyColumnHeaderWidths();
this.updateCanvasWidth(true);
- this.trigger(this.onAutosizeColumns, { columns: this.columns });
+ this.triggerEvent(this.onAutosizeColumns, { columns: this.columns });
if (reRender) {
this.invalidateAllRows();
@@ -2733,7 +2733,7 @@ export class SlickGrid = Column, O e
// General
- protected trigger(evt: SlickEvent, args?: ArgType, e?: Event | SlickEventData) {
+ triggerEvent(evt: SlickEvent, args?: ArgType, e?: Event | SlickEventData) {
const event: SlickEventData = (e || new SlickEventData(e, args)) as SlickEventData;
const eventArgs = (args || {}) as ArgType & { grid: SlickGrid; };
eventArgs.grid = this;
@@ -2906,7 +2906,7 @@ export class SlickGrid = Column, O e
const newSelectedAdditions = this.getSelectedRows().filter((i) => previousSelectedRows.indexOf(i) < 0);
const newSelectedDeletions = previousSelectedRows.filter((i) => this.getSelectedRows().indexOf(i) < 0);
- this.trigger(this.onSelectedRowsChanged, {
+ this.triggerEvent(this.onSelectedRowsChanged, {
rows: this.getSelectedRows(),
previousSelectedRows,
caller,
@@ -2974,14 +2974,14 @@ export class SlickGrid = Column, O e
* @param {Column[]} columnDefinitions An array of column definitions.
*/
setColumns(columnDefinitions: C[]) {
- this.trigger(this.onBeforeSetColumns, { previousColumns: this.columns, newColumns: columnDefinitions, grid: this });
+ this.triggerEvent(this.onBeforeSetColumns, { previousColumns: this.columns, newColumns: columnDefinitions, grid: this });
this.columns = columnDefinitions;
this.updateColumnsInternal();
}
/** Update columns for when a hidden property has changed but the column list itself has not changed. */
updateColumns() {
- this.trigger(this.onBeforeUpdateColumns, { columns: this.columns, grid: this });
+ this.triggerEvent(this.onBeforeUpdateColumns, { columns: this.columns, grid: this });
this.updateColumnsInternal();
}
@@ -3032,7 +3032,7 @@ export class SlickGrid = Column, O e
const originalOptions = extend(true, {}, this._options);
this._options = extend(this._options, newOptions);
- this.trigger(this.onSetOptions, { optionsBefore: originalOptions, optionsAfter: this._options });
+ this.triggerEvent(this.onSetOptions, { optionsBefore: originalOptions, optionsAfter: this._options });
this.internal_setOptions(suppressRender, suppressColumnSet, suppressSetOverflow);
}
@@ -3049,7 +3049,7 @@ export class SlickGrid = Column, O e
this.prepareForOptionsChange();
this.invalidateRow(this.getDataLength());
- this.trigger(this.onActivateChangedOptions, { options: this._options });
+ this.triggerEvent(this.onActivateChangedOptions, { options: this._options });
this.internal_setOptions(suppressRender, suppressColumnSet, suppressSetOverflow);
}
@@ -3272,7 +3272,7 @@ export class SlickGrid = Column, O e
this._viewportScrollContainerY.scrollTop = newScrollTop;
}
- this.trigger(this.onViewportChanged, {});
+ this.triggerEvent(this.onViewportChanged, {});
}
}
@@ -3425,7 +3425,7 @@ export class SlickGrid = Column, O e
// get addl css class names from object type formatter return and from string type return of onBeforeAppendCell
// we will only use the event result as CSS classes when it is a string type (undefined event always return a true boolean which is not a valid css class)
- const evt = this.trigger(this.onBeforeAppendCell, { row, cell, value, dataContext: item });
+ const evt = this.triggerEvent(this.onBeforeAppendCell, { row, cell, value, dataContext: item });
const appendCellResult = evt.getReturnValue();
let addlCssClasses = typeof appendCellResult === 'string' ? appendCellResult : '';
if ((formatterResult as FormatterResultObject)?.addClasses) {
@@ -4346,7 +4346,7 @@ export class SlickGrid = Column, O e
this.lastRenderedScrollTop = this.scrollTop;
this.lastRenderedScrollLeft = this.scrollLeft;
this.h_render = null;
- this.trigger(this.onRendered, { startRow: visible.top, endRow: visible.bottom, grid: this });
+ this.triggerEvent(this.onRendered, { startRow: visible.top, endRow: visible.bottom, grid: this });
}
protected handleHeaderRowScroll() {
@@ -4479,11 +4479,11 @@ export class SlickGrid = Column, O e
this.scrollThrottle.enqueue();
}
- this.trigger(this.onViewportChanged, {});
+ this.triggerEvent(this.onViewportChanged, {});
}
}
- this.trigger(this.onScroll, { scrollLeft: this.scrollLeft, scrollTop: this.scrollTop });
+ this.triggerEvent(this.onScroll, { scrollLeft: this.scrollLeft, scrollTop: this.scrollTop });
if (hScrollDist || vScrollDist) { return true; }
return false;
@@ -4649,7 +4649,7 @@ export class SlickGrid = Column, O e
this.cellCssClasses[key] = hash;
this.updateCellCssStylesOnRenderedRows(hash, null);
- this.trigger(this.onCellCssStylesChanged, { key, hash, grid: this });
+ this.triggerEvent(this.onCellCssStylesChanged, { key, hash, grid: this });
}
/**
@@ -4663,7 +4663,7 @@ export class SlickGrid = Column, O e
this.updateCellCssStylesOnRenderedRows(null, this.cellCssClasses[key]);
delete this.cellCssClasses[key];
- this.trigger(this.onCellCssStylesChanged, { key, hash: null, grid: this });
+ this.triggerEvent(this.onCellCssStylesChanged, { key, hash: null, grid: this });
}
/**
@@ -4679,7 +4679,7 @@ export class SlickGrid = Column, O e
this.cellCssClasses[key] = hash;
this.updateCellCssStylesOnRenderedRows(hash, prevHash);
- this.trigger(this.onCellCssStylesChanged, { key, hash, grid: this });
+ this.triggerEvent(this.onCellCssStylesChanged, { key, hash, grid: this });
}
/**
@@ -4756,7 +4756,7 @@ export class SlickGrid = Column, O e
return false;
}
- const retval = this.trigger(this.onDragInit, dd, e);
+ const retval = this.triggerEvent(this.onDragInit, dd, e);
if (retval.isImmediatePropagationStopped()) {
return retval.getReturnValue();
}
@@ -4772,7 +4772,7 @@ export class SlickGrid = Column, O e
return false;
}
- const retval = this.trigger(this.onDragStart, dd, e);
+ const retval = this.triggerEvent(this.onDragStart, dd, e);
if (retval.isImmediatePropagationStopped()) {
return retval.getReturnValue();
}
@@ -4781,15 +4781,15 @@ export class SlickGrid = Column, O e
}
protected handleDrag(e: DragEvent, dd: DragPosition) {
- return this.trigger(this.onDrag, dd, e).getReturnValue();
+ return this.triggerEvent(this.onDrag, dd, e).getReturnValue();
}
protected handleDragEnd(e: DragEvent, dd: DragPosition) {
- this.trigger(this.onDragEnd, dd, e);
+ this.triggerEvent(this.onDragEnd, dd, e);
}
protected handleKeyDown(e: KeyboardEvent & { originalEvent: Event; }) {
- const retval = this.trigger(this.onKeyDown, { row: this.activeRow, cell: this.activeCell }, e);
+ const retval = this.triggerEvent(this.onKeyDown, { row: this.activeRow, cell: this.activeCell }, e);
let handled: boolean | undefined | void = retval.isImmediatePropagationStopped();
if (!handled) {
@@ -4883,7 +4883,7 @@ export class SlickGrid = Column, O e
return;
}
- evt = this.trigger(this.onClick, { row: cell.row, cell: cell.cell }, evt || e);
+ evt = this.triggerEvent(this.onClick, { row: cell.row, cell: cell.cell }, evt || e);
if ((evt as any).isImmediatePropagationStopped()) {
return;
}
@@ -4913,7 +4913,7 @@ export class SlickGrid = Column, O e
return;
}
- this.trigger(this.onContextMenu, {}, e);
+ this.triggerEvent(this.onContextMenu, {}, e);
}
protected handleDblClick(e: MouseEvent) {
@@ -4922,7 +4922,7 @@ export class SlickGrid = Column, O e
return;
}
- this.trigger(this.onDblClick, { row: cell.row, cell: cell.cell }, e);
+ this.triggerEvent(this.onDblClick, { row: cell.row, cell: cell.cell }, e);
if (e.defaultPrevented) {
return;
}
@@ -4937,7 +4937,7 @@ export class SlickGrid = Column, O e
if (!c) {
return;
}
- this.trigger(this.onHeaderMouseEnter, {
+ this.triggerEvent(this.onHeaderMouseEnter, {
column: c,
grid: this
}, e);
@@ -4948,7 +4948,7 @@ export class SlickGrid = Column, O e
if (!c) {
return;
}
- this.trigger(this.onHeaderMouseLeave, {
+ this.triggerEvent(this.onHeaderMouseLeave, {
column: c,
grid: this
}, e);
@@ -4959,7 +4959,7 @@ export class SlickGrid = Column, O e
if (!c) {
return;
}
- this.trigger(this.onHeaderRowMouseEnter, {
+ this.triggerEvent(this.onHeaderRowMouseEnter, {
column: c,
grid: this
}, e);
@@ -4970,7 +4970,7 @@ export class SlickGrid = Column, O e
if (!c) {
return;
}
- this.trigger(this.onHeaderRowMouseLeave, {
+ this.triggerEvent(this.onHeaderRowMouseLeave, {
column: c,
grid: this
}, e);
@@ -4979,7 +4979,7 @@ export class SlickGrid = Column, O e
protected handleHeaderContextMenu(e: MouseEvent & { target: HTMLElement; }) {
const header = e.target.closest('.slick-header-column');
const column = header && Utils.storage.get(header, 'column');
- this.trigger(this.onHeaderContextMenu, { column }, e);
+ this.triggerEvent(this.onHeaderContextMenu, { column }, e);
}
protected handleHeaderClick(e: MouseEvent & { target: HTMLElement; }) {
@@ -4990,28 +4990,28 @@ export class SlickGrid = Column, O e
const header = e.target.closest('.slick-header-column');
const column = header && Utils.storage.get(header, 'column');
if (column) {
- this.trigger(this.onHeaderClick, { column }, e);
+ this.triggerEvent(this.onHeaderClick, { column }, e);
}
}
protected handleFooterContextMenu(e: MouseEvent & { target: HTMLElement; }) {
const footer = e.target.closest('.slick-footerrow-column');
const column = footer && Utils.storage.get(footer, 'column');
- this.trigger(this.onFooterContextMenu, { column }, e);
+ this.triggerEvent(this.onFooterContextMenu, { column }, e);
}
protected handleFooterClick(e: MouseEvent & { target: HTMLElement; }) {
const footer = e.target.closest('.slick-footerrow-column');
const column = footer && Utils.storage.get(footer, 'column');
- this.trigger(this.onFooterClick, { column }, e);
+ this.triggerEvent(this.onFooterClick, { column }, e);
}
protected handleCellMouseOver(e: MouseEvent & { target: HTMLElement; }) {
- this.trigger(this.onMouseEnter, {}, e);
+ this.triggerEvent(this.onMouseEnter, {}, e);
}
protected handleCellMouseOut(e: MouseEvent & { target: HTMLElement; }) {
- this.trigger(this.onMouseLeave, {}, e);
+ this.triggerEvent(this.onMouseLeave, {}, e);
}
protected cellExists(row: number, cell: number) {
@@ -5273,7 +5273,7 @@ export class SlickGrid = Column, O e
// this optimisation causes trouble - MLeibman #329
// if (activeCellChanged) {
if (!suppressActiveCellChangedEvent) {
- this.trigger(this.onActiveCellChanged, this.getActiveCell() as OnActiveCellChangedEventArgs);
+ this.triggerEvent(this.onActiveCellChanged, this.getActiveCell() as OnActiveCellChangedEventArgs);
}
// }
}
@@ -5322,7 +5322,7 @@ export class SlickGrid = Column, O e
if (!this.currentEditor) {
return;
}
- this.trigger(this.onBeforeCellEditorDestroy, { editor: this.currentEditor });
+ this.triggerEvent(this.onBeforeCellEditorDestroy, { editor: this.currentEditor });
this.currentEditor.destroy();
this.currentEditor = null;
@@ -5374,7 +5374,7 @@ export class SlickGrid = Column, O e
const columnDef = this.columns[this.activeCell];
const item = this.getDataItem(this.activeRow);
- if (this.trigger(this.onBeforeEditCell, { row: this.activeRow, cell: this.activeCell, item, column: columnDef, target: 'grid' }).getReturnValue() === false) {
+ if (this.triggerEvent(this.onBeforeEditCell, { row: this.activeRow, cell: this.activeCell, item, column: columnDef, target: 'grid' }).getReturnValue() === false) {
this.setFocus();
return;
}
@@ -5497,7 +5497,7 @@ export class SlickGrid = Column, O e
return;
}
- this.trigger(this.onActiveCellPositionChanged, {});
+ this.triggerEvent(this.onActiveCellPositionChanged, {});
if (this.currentEditor) {
const cellBox = this.getActiveCellPosition();
@@ -6208,12 +6208,12 @@ export class SlickGrid = Column, O e
execute: () => {
editor.applyValue(item, serializedValue);
self.updateRow(row);
- self.trigger(self.onCellChange, { command: 'execute', row, cell, item, column });
+ self.triggerEvent(self.onCellChange, { command: 'execute', row, cell, item, column });
},
undo: () => {
editor.applyValue(item, prevSerializedValue);
self.updateRow(row);
- self.trigger(self.onCellChange, { command: 'undo', row, cell, item, column, });
+ self.triggerEvent(self.onCellChange, { command: 'undo', row, cell, item, column, });
}
};
@@ -6229,7 +6229,7 @@ export class SlickGrid = Column, O e
const newItem = {};
self.currentEditor.applyValue(newItem, self.currentEditor.serializeValue());
self.makeActiveCellNormal(true);
- self.trigger(self.onAddNewRow, { item: newItem, column });
+ self.triggerEvent(self.onAddNewRow, { item: newItem, column });
}
// check whether the lock has been re-acquired by event handlers
@@ -6242,7 +6242,7 @@ export class SlickGrid = Column, O e
self.activeCellNode.classList.add('invalid');
}
- self.trigger(self.onValidationError, {
+ self.triggerEvent(self.onValidationError, {
editor: self.currentEditor,
cellNode: self.activeCellNode,
validationResults,
diff --git a/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts b/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts
index 385324548..2138ac84b 100644
--- a/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts
+++ b/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts
@@ -1,7 +1,7 @@
import 'jest-extended';
import { SelectionModel } from '../../enums/index';
-import type { Column, GridOption, } from '../../interfaces/index';
+import type { Column, GridOption, OnEventArgs, } from '../../interfaces/index';
import { SlickCellSelectionModel } from '../slickCellSelectionModel';
import { SlickCellExternalCopyManager } from '../slickCellExternalCopyManager';
import { InputEditor } from '../../editors/inputEditor';
@@ -12,9 +12,13 @@ jest.mock('flatpickr', () => { });
const mockGetSelectionModel = {
getSelectedRanges: jest.fn(),
};
+const returnValueStub = jest.fn();
const gridStub = {
getActiveCell: jest.fn(),
- getColumns: jest.fn(),
+ getColumns: jest.fn().mockReturnValue([
+ { id: 'firstName', field: 'firstName', name: 'First Name', },
+ { id: 'lastName', field: 'lastName', name: 'Last Name' },
+ ]),
getData: jest.fn(),
getDataItem: jest.fn(),
getDataLength: jest.fn(),
@@ -31,6 +35,7 @@ const gridStub = {
setSelectionModel: jest.fn(),
updateCell: jest.fn(),
render: jest.fn(),
+ triggerEvent: jest.fn().mockReturnValue({ getReturnValue: returnValueStub }),
onCellChange: new SlickEvent(),
onKeyDown: new SlickEvent(),
} as unknown as SlickGrid;
@@ -196,6 +201,23 @@ describe('CellExternalCopyManager', () => {
expect(plugin.addonOptions.includeHeaderWhenCopying).toBeTruthy();
});
+ it('should call onBeforePasteCell with current row and column info', () => {
+ const sutSpy = jest.fn();
+ plugin.init(gridStub, { onBeforePasteCell: sutSpy });
+
+ plugin.onBeforePasteCell.notify({
+ row: 0,
+ cell: 0,
+ item: {
+ firstName: 'John',
+ lastName: 'Doe'
+ },
+ value: 'Foobar', columnDef: {} as Column
+ });
+
+ expect(sutSpy).toHaveBeenCalled();
+ });
+
describe('keyDown handler', () => {
beforeEach(() => {
jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns);
@@ -331,6 +353,49 @@ describe('CellExternalCopyManager', () => {
}, 2);
});
+ it('should not paste on cells where onBeforePasteCell handler returns false', (done) => {
+ let clipCommand;
+ const clipboardCommandHandler = (cmd) => {
+ clipCommand = cmd;
+ cmd.execute();
+ };
+ jest.spyOn(gridStub.getSelectionModel() as SelectionModel, 'getSelectedRanges').mockReturnValueOnce([new SlickRange(0, 1, 1, 2)]).mockReturnValueOnce(null as any);
+
+ // first one should be denied
+ returnValueStub.mockReturnValueOnce(false);
+ plugin.init(gridStub, { clipboardPasteDelay: 1, clearCopySelectionDelay: 1, includeHeaderWhenCopying: true, clipboardCommandHandler, onBeforePasteCell: (e: SlickEventData, args: OnEventArgs) => args.cell > 0 });
+
+ const keyDownCtrlCopyEvent = new Event('keydown');
+ Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true });
+ Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' });
+ Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() });
+ Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() });
+ gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub);
+
+ const updateCellSpy = jest.spyOn(gridStub, 'updateCell');
+ const onCellChangeSpy = jest.spyOn(gridStub.onCellChange, 'notify');
+ const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 0, row: 1 });
+ const keyDownCtrlPasteEvent = new Event('keydown');
+ jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns);
+ Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true });
+ Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' });
+ Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() });
+ Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() });
+ gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub);
+ document.querySelector('textarea')!.value = `Doe\tserialized output`;
+
+ setTimeout(() => {
+ expect(getActiveCellSpy).toHaveBeenCalled();
+ expect(updateCellSpy).not.toHaveBeenCalledWith(1, 0);
+ expect(updateCellSpy).toHaveBeenCalledWith(1, 1);
+ expect(onCellChangeSpy).toHaveBeenCalledWith({ row: 1, cell: 1, item: { firstName: 'John', lastName: 'serialized output' }, grid: gridStub, column: {} });
+ const getDataItemSpy = jest.spyOn(gridStub, 'getDataItem');
+ clipCommand.undo();
+ expect(getDataItemSpy).toHaveBeenCalled();
+ done();
+ }, 2);
+ });
+
it('should Copy, Paste and run Execute clip command with only 1 cell to copy', (done) => {
jest.spyOn(gridStub.getSelectionModel() as SelectionModel, 'getSelectedRanges').mockReturnValueOnce([new SlickRange(0, 1, 1, 2)]).mockReturnValueOnce([new SlickRange(0, 1, 1, 2)]);
let clipCommand;
diff --git a/packages/common/src/extensions/slickCellExternalCopyManager.ts b/packages/common/src/extensions/slickCellExternalCopyManager.ts
index 7345bd7eb..c24bca8d4 100644
--- a/packages/common/src/extensions/slickCellExternalCopyManager.ts
+++ b/packages/common/src/extensions/slickCellExternalCopyManager.ts
@@ -1,7 +1,7 @@
import { createDomElement, stripTags } from '@slickgrid-universal/utils';
-import type { Column, ExcelCopyBufferOption, ExternalCopyClipCommand } from '../interfaces/index';
-import { SlickEvent, SlickEventData, SlickEventHandler, type SlickGrid, SlickRange } from '../core/index';
+import type { Column, ExcelCopyBufferOption, ExternalCopyClipCommand, OnEventArgs } from '../interfaces/index';
+import { SlickEvent, SlickEventData, SlickEventHandler, type SlickGrid, SlickRange, SlickDataView } from '../core/index';
// using external SlickGrid JS libraries
const CLEAR_COPY_SELECTION_DELAY = 2000;
@@ -20,6 +20,7 @@ export class SlickCellExternalCopyManager {
onCopyCells = new SlickEvent<{ ranges: SlickRange[]; }>();
onCopyCancelled = new SlickEvent<{ ranges: SlickRange[]; }>();
onPasteCells = new SlickEvent<{ ranges: SlickRange[]; }>();
+ onBeforePasteCell = new SlickEvent<{ cell: number; row: number; item: any; columnDef: Column; value: any; }>();
protected _addonOptions!: ExcelCopyBufferOption;
protected _bodyElement = document.body;
@@ -68,6 +69,26 @@ export class SlickCellExternalCopyManager {
this._grid.focus();
}
});
+
+ if (grid && typeof this._addonOptions?.onBeforePasteCell === 'function') {
+ const dataView = grid?.getData();
+
+ // subscribe to this Slickgrid event of onBeforeEditCell
+ this._eventHandler.subscribe(this.onBeforePasteCell, (e, args) => {
+ const column: Column = grid.getColumns()[args.cell];
+ const returnedArgs: OnEventArgs = {
+ row: args.row!,
+ cell: args.cell,
+ dataView,
+ grid,
+ columnDef: column,
+ dataContext: grid.getDataItem(args.row!)
+ };
+
+ // finally call up the Slick column.onBeforeEditCells.... function
+ return this._addonOptions.onBeforePasteCell?.(e, returnedArgs);
+ });
+ }
}
dispose() {
@@ -258,6 +279,11 @@ export class SlickCellExternalCopyManager {
if (desty < clipCommand.maxDestY && destx < clipCommand.maxDestX) {
// const nd = this._grid.getCellNode(desty, destx);
const dt = this._grid.getDataItem(desty);
+
+ if (this._grid.triggerEvent(this.onBeforePasteCell, { row: desty, cell: destx, dt, column: columns[destx], target: 'grid' }).getReturnValue() === false) {
+ continue;
+ }
+
clipCommand.oldValues[y][x] = dt[columns[destx]['field']];
if (oneCellToMultiple) {
this.setDataItemValueForColumn(dt, columns[destx], clippedRange[0][0]);
@@ -345,7 +371,6 @@ export class SlickCellExternalCopyManager {
}
}
-
protected handleKeyDown(e: any): boolean | void {
let ranges: SlickRange[];
if (!this._grid.getEditorLock().isActive() || this._grid.getOptions().autoEdit) {
diff --git a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts
index a4065a0f3..98396ed59 100644
--- a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts
+++ b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts
@@ -1,5 +1,5 @@
-import type { Column, FormatterResultWithHtml, FormatterResultWithText, } from './index';
+import type { Column, FormatterResultWithHtml, FormatterResultWithText, OnEventArgs, } from './index';
import type { SlickCellExcelCopyManager, } from '../extensions/slickCellExcelCopyManager';
import type { SlickEventData, SlickRange } from '../core/index';
@@ -61,4 +61,7 @@ export interface ExcelCopyBufferOption {
/** Fired when the user paste cells to the grid */
onPasteCells?: (e: SlickEventData, args: { ranges: SlickRange[]; }) => void;
+
+ /** Fired for each cell before pasting. Return false if you want to deny pasting for the specific cell */
+ onBeforePasteCell?: (e: SlickEventData, args: OnEventArgs) => boolean;
}
diff --git a/packages/common/src/services/gridEvent.service.ts b/packages/common/src/services/gridEvent.service.ts
index 1c5bc7238..0b3629313 100644
--- a/packages/common/src/services/gridEvent.service.ts
+++ b/packages/common/src/services/gridEvent.service.ts
@@ -16,7 +16,7 @@ export class GridEventService {
this._eventHandler.unsubscribeAll();
}
- /* OnCellChange Event */
+ /* OnBeforeEditCell Event */
bindOnBeforeEditCell(grid: SlickGrid) {
const dataView = grid?.getData();