Skip to content

Commit cf622cc

Browse files
authored
Merge pull request #5386 from IgniteUI/rkaraivanov/grid-clipboard-master
feat(igx-grid): Copy into clipboard behavior
2 parents c237e93 + 6c26529 commit cf622cc

19 files changed

+836
-469
lines changed

projects/igniteui-angular/src/lib/core/grid-selection.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,11 +371,14 @@ export class IgxGridSelectionService {
371371
}
372372
}
373373

374-
keyboardStateOnFocus(node: ISelectionNode, emitter: EventEmitter<GridSelectionRange>): void {
374+
keyboardStateOnFocus(node: ISelectionNode, emitter: EventEmitter<GridSelectionRange>, dom): void {
375375
const kbState = this.keyboardState;
376376

377377
// Focus triggered by keyboard navigation
378378
if (kbState.active) {
379+
if (isChromium()) {
380+
this._moveSelectionChrome(dom);
381+
}
379382
// Start generating a range if shift is hold
380383
if (kbState.shift) {
381384
this.dragSelect(node, kbState);
@@ -525,6 +528,11 @@ export class IgxGridSelectionService {
525528
}
526529
}
527530

531+
/**
532+
* (╯°□°)╯︵ ┻━┻
533+
* Chrome and Chromium don't care about the active
534+
* range after keyboard navigation, thus this.
535+
*/
528536
_moveSelectionChrome(node: Node) {
529537
const selection = window.getSelection();
530538
selection.removeAllRanges();
@@ -534,3 +542,7 @@ export class IgxGridSelectionService {
534542
selection.addRange(range);
535543
}
536544
}
545+
546+
export function isChromium(): boolean {
547+
return (/Chrom|e?ium/g.test(navigator.userAgent) || /Google Inc/g.test(navigator.vendor)) && !/Edge/g.test(navigator.userAgent);
548+
}

projects/igniteui-angular/src/lib/grids/cell.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<ng-template #defaultCell>
2-
<div igxTextHighlight [cssClass]="highlightClass" [activeCssClass]="activeHighlightClass" [groupName]="gridID"
2+
<div igxTextHighlight style="pointer-events: none" [cssClass]="highlightClass" [activeCssClass]="activeHighlightClass" [groupName]="gridID"
33
[value]="formatter ? formatter(value) : column.dataType === 'number' ? (value | igxdecimal: grid.locale) : column.dataType === 'date' ? (value | igxdate: grid.locale) : value"
44
[row]="rowData" [column]="this.column.field" [containerClass]="'igx-grid__td-text'"
55
class="igx-grid__td-text">{{ formatter ? formatter(value) : column.dataType === 'number' ? (value | igxdecimal:

projects/igniteui-angular/src/lib/grids/cell.component.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,6 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy {
520520
protected isInCompositionMode = false;
521521
protected compositionStartHandler;
522522
protected compositionEndHandler;
523-
protected focusHandlerIE;
524-
protected focusOut;
525523
private _highlight: IgxTextHighlightDirective;
526524

527525

@@ -552,12 +550,6 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy {
552550
// Hitting Enter with IME submits and exits from edit mode instead of first closing the IME dialog
553551
this.nativeElement.addEventListener('compositionstart', this.compositionStartHandler);
554552
this.nativeElement.addEventListener('compositionend', this.compositionEndHandler);
555-
556-
// https://stackoverflow.com/q/51404782
557-
this.focusHandlerIE = (e: FocusEvent) => this.onFocus(e);
558-
this.focusOut = () => this.onBlur();
559-
this.nativeElement.addEventListener('focusin', this.focusHandlerIE);
560-
this.nativeElement.addEventListener('focusout', this.focusOut);
561553
}
562554
});
563555
}
@@ -575,8 +567,6 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy {
575567
if (isIE()) {
576568
this.nativeElement.removeEventListener('compositionstart', this.compositionStartHandler);
577569
this.nativeElement.removeEventListener('compositionend', this.compositionEndHandler);
578-
this.nativeElement.removeEventListener('focusin', this.focusHandlerIE);
579-
this.nativeElement.removeEventListener('focusout', this.focusOut);
580570
}
581571
});
582572
}
@@ -792,7 +782,7 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy {
792782
}
793783

794784
this.selectionService.primaryButton = true;
795-
this.selectionService.keyboardStateOnFocus(node, this.grid.onRangeSelection);
785+
this.selectionService.keyboardStateOnFocus(node, this.grid.onRangeSelection, this.nativeElement);
796786
}
797787

798788
/**

projects/igniteui-angular/src/lib/grids/grid-base.component.ts

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
import { Subject } from 'rxjs';
3131
import { takeUntil, first, filter } from 'rxjs/operators';
3232
import { IgxSelectionAPIService } from '../core/selection';
33-
import { cloneArray, isEdge, isNavigationKey, CancelableEventArgs, flatten, mergeObjects } from '../core/utils';
33+
import { cloneArray, isEdge, isNavigationKey, CancelableEventArgs, flatten, mergeObjects, isIE } from '../core/utils';
3434
import { DataType } from '../data-operations/data-util';
3535
import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface';
3636
import { IGroupByRecord } from '../data-operations/groupby-record.interface';
@@ -91,6 +91,7 @@ import { IgxGridFilteringRowComponent } from './filtering/grid-filtering-row.com
9191
import { IgxDragIndicatorIconDirective } from './row-drag.directive';
9292
import { IgxDragDirective } from '../directives/dragdrop/dragdrop.directive';
9393
import { DeprecateProperty } from '../core/deprecateDecorators';
94+
import { CharSeparatedValueData } from '../services/csv/char-separated-value-data';
9495

9596
const MINIMUM_COLUMN_WIDTH = 136;
9697
const FILTER_ROW_HEIGHT = 50;
@@ -104,6 +105,11 @@ const MIN_ROW_EDITING_COUNT_THRESHOLD = 2;
104105

105106
export const IgxGridTransaction = new InjectionToken<string>('IgxGridTransaction');
106107

108+
export interface IGridClipboardEvent {
109+
data: any[];
110+
cancel: boolean;
111+
}
112+
107113
export interface IGridCellEventArgs {
108114
cell: IgxGridCellComponent;
109115
event: Event;
@@ -1519,6 +1525,13 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
15191525
@Output()
15201526
public onRowDragEnd = new EventEmitter<IRowDragEndEventArgs>();
15211527

1528+
/**
1529+
* Emitted when a copy operation is executed.
1530+
* Fired only if copy behavior is enabled through the [`clipboardOptions`]{@link IgxGridBaseComponent#clipboardOptions}.
1531+
*/
1532+
@Output()
1533+
onGridCopy = new EventEmitter<IGridClipboardEvent>();
1534+
15221535
/**
15231536
* @hidden
15241537
*/
@@ -2297,6 +2310,29 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
22972310
}
22982311
}
22992312

2313+
/**
2314+
* Controls the copy behavior of the grid.
2315+
*/
2316+
@Input()
2317+
clipboardOptions = {
2318+
/**
2319+
* Enables/disables the copy behavior
2320+
*/
2321+
enabled: true,
2322+
/**
2323+
* Include the columns headers in the clipboard output.
2324+
*/
2325+
copyHeaders: true,
2326+
/**
2327+
* Apply the columns formatters (if any) on the data in the clipboard output.
2328+
*/
2329+
copyFormatters: true,
2330+
/**
2331+
* The separator used for formatting the copy output. Defaults to `\t`.
2332+
*/
2333+
separator: '\t'
2334+
};
2335+
23002336
/**
23012337
* @hidden
23022338
*/
@@ -2316,7 +2352,10 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
23162352

23172353
/* End of toolbar related definitions */
23182354

2319-
// TODO: Document
2355+
/**
2356+
* Emitted when making a range selection either through
2357+
* drag selection or through keyboard selection.
2358+
*/
23202359
@Output()
23212360
onRangeSelection = new EventEmitter<GridSelectionRange>();
23222361

@@ -2561,6 +2600,10 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
25612600

25622601
private keydownHandler(event) {
25632602
const key = event.key.toLowerCase();
2603+
// TODO: Move in a separate handler on the `grid body`.
2604+
// if (event.ctrlKey && key === 'c' && isIE()) {
2605+
// this.copyHandler(null, true);
2606+
// }
25642607
if ((isNavigationKey(key) && event.keyCode !== 32) || key === 'tab' || key === 'pagedown' || key === 'pageup') {
25652608
event.preventDefault();
25662609
if (key === 'pagedown') {
@@ -4783,7 +4826,8 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
47834826
return this.selectionService.ranges;
47844827
}
47854828

4786-
extractDataFromSelection(source: any[]): any[] {
4829+
4830+
protected extractDataFromSelection(source: any[], formatters = false, headers = false): any[] {
47874831
let columnsArray: IgxColumnComponent[];
47884832
let record = {};
47894833
const selectedData = [];
@@ -4801,7 +4845,9 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
48014845
columnsArray = this.getSelectableColumnsAt(each);
48024846
columnsArray.forEach((col) => {
48034847
if (col) {
4804-
record[col.field] = source[row][col.field];
4848+
const key = headers ? col.header || col.field : col.field;
4849+
record[key] = formatters && col.formatter ? col.formatter(source[row][col.field])
4850+
: source[row][col.field];
48054851
}
48064852
});
48074853
}
@@ -4828,10 +4874,15 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
48284874
}
48294875
}
48304876

4831-
getSelectedData() {
4877+
/**
4878+
*
4879+
* Returns an array of the current cell selection in the form of `[{ column.field: cell.value }, ...]`.
4880+
* If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any).
4881+
* If `headers` is enabled, it will use the column header (if any) instead of the column field.
4882+
*/
4883+
getSelectedData(formatters = false, headers = false) {
48324884
const source = this.verticalScrollContainer.igxForOf;
4833-
4834-
return this.extractDataFromSelection(source);
4885+
return this.extractDataFromSelection(source, formatters, headers);
48354886
}
48364887

48374888
/**
@@ -4855,14 +4906,57 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements
48554906
/**
48564907
* @hidden
48574908
*/
4858-
// @HostListener('scroll', ['$event'])
48594909
public scrollHandler(event) {
48604910
this.parentVirtDir.getHorizontalScroll().scrollLeft += event.target.scrollLeft;
48614911
this.verticalScrollContainer.getVerticalScroll().scrollTop += event.target.scrollTop;
48624912
event.target.scrollLeft = 0;
48634913
event.target.scrollTop = 0;
48644914
}
48654915

4916+
copyHandlerIE() {
4917+
if (isIE()) {
4918+
this.copyHandler(null, true);
4919+
}
4920+
}
4921+
4922+
/**
4923+
* @hidden
4924+
* @internal
4925+
*/
4926+
public copyHandler(event, ie11 = false) {
4927+
if (!this.clipboardOptions.enabled || this.crudService.inEditMode) {
4928+
return;
4929+
}
4930+
4931+
const data = this.getSelectedData(this.clipboardOptions.copyFormatters, this.clipboardOptions.copyHeaders);
4932+
const ev = { data, cancel: false } as IGridClipboardEvent;
4933+
this.onGridCopy.emit(ev);
4934+
4935+
if (ev.cancel) {
4936+
return;
4937+
}
4938+
4939+
const transformer = new CharSeparatedValueData(ev.data, this.clipboardOptions.separator);
4940+
let result = transformer.prepareData();
4941+
4942+
if (!this.clipboardOptions.copyHeaders) {
4943+
result = result.substring(result.indexOf('\n') + 1);
4944+
}
4945+
4946+
if (ie11) {
4947+
(window as any).clipboardData.setData('Text', result);
4948+
return;
4949+
}
4950+
4951+
event.preventDefault();
4952+
4953+
/* Necessary for the hiearachical case but will probably have to
4954+
change how getSelectedData is propagated in the hiearachical grid
4955+
*/
4956+
event.stopPropagation();
4957+
event.clipboardData.setData('text/plain', result);
4958+
}
4959+
48664960
/**
48674961
* This method allows you to navigate to a position
48684962
* in the grid based on provided `rowindex` and `visibleColumnIndex`,

0 commit comments

Comments
 (0)