+ [tabIndex]="isSelectable ? 0 : -1"
+ [class.mat-selected]="(isClickable || isSelectable) && isRowSelected(row)"
+ *ngFor="let row of data; let rowIndex = index"
+ (click)="handleRowClick(row, $event, rowIndex)"
+ (keyup)="isSelectable && _rowKeyup($event, row, rowIndex)"
+ (keydown.space)="blockEvent($event)"
+ (keydown.shift.space)="blockEvent($event)"
+ (keydown.shift)="disableTextSelection()"
+ (keyup.shift)="enableTextSelection()">
+ [state]="isRowSelected(row) ? 'checked' : 'unchecked'"
+ (mousedown)="disableTextSelection()"
+ (mouseup)="enableTextSelection()"
+ (click)="select(row, $event, rowIndex)">
|
{
CovalentDataTableModule,
],
declarations: [
- TdDataTableBasicComponent,
+ TdDataTableBasicTestComponent,
TdDataTableSelectableTestComponent,
+ TdDataTableRowClickTestComponent,
],
providers: [
TdDataTableService,
@@ -37,8 +39,8 @@ describe('Component: DataTable', () => {
it('should set hidden and not get search hits and set it to false and get search results', (done: DoneFn) => {
inject([TdDataTableService], (tdDataTableService: TdDataTableService) => {
- let fixture: ComponentFixture = TestBed.createComponent(TdDataTableBasicComponent);
- let component: TdDataTableBasicComponent = fixture.debugElement.componentInstance;
+ let fixture: ComponentFixture = TestBed.createComponent(TdDataTableBasicTestComponent);
+ let component: TdDataTableBasicTestComponent = fixture.debugElement.componentInstance;
component.columns[1].hidden = false;
// backwards compatability test
@@ -85,8 +87,8 @@ describe('Component: DataTable', () => {
it('should set filter and not get search hits and set it to false and get search results', (done: DoneFn) => {
inject([TdDataTableService], (tdDataTableService: TdDataTableService) => {
- let fixture: ComponentFixture = TestBed.createComponent(TdDataTableBasicComponent);
- let component: TdDataTableBasicComponent = fixture.debugElement.componentInstance;
+ let fixture: ComponentFixture = TestBed.createComponent(TdDataTableBasicTestComponent);
+ let component: TdDataTableBasicTestComponent = fixture.debugElement.componentInstance;
component.columns[1].filter = false;
@@ -169,7 +171,7 @@ describe('Component: DataTable', () => {
expect(dataTableComponent.indeterminate).toBeFalsy();
expect(dataTableComponent.allSelected).toBeFalsy();
// select a row with a click event
- fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[2].triggerEventHandler('click', new Event('click'));
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[2].triggerEventHandler('click', new Event('click'));
fixture.detectChanges();
fixture.whenStable().then(() => {
// check to see if its in indeterminate state
@@ -225,23 +227,23 @@ describe('Component: DataTable', () => {
expect(dataTableComponent.indeterminate).toBeFalsy();
expect(dataTableComponent.allSelected).toBeFalsy();
// select a row with a click event
- fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[2].triggerEventHandler('click', new Event('click'));
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[2].triggerEventHandler('click', new Event('click'));
fixture.detectChanges();
fixture.whenStable().then(() => {
// check to see if its in indeterminate state
expect(dataTableComponent.indeterminate).toBeTruthy();
expect(dataTableComponent.allSelected).toBeFalsy();
// select the rest of the rows
- fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[1].triggerEventHandler('click', new Event('click'));
- fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[3].triggerEventHandler('click', new Event('click'));
- fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[4].triggerEventHandler('click', new Event('click'));
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[0].triggerEventHandler('click', new Event('click'));
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[1].triggerEventHandler('click', new Event('click'));
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[3].triggerEventHandler('click', new Event('click'));
fixture.detectChanges();
fixture.whenStable().then(() => {
// check to see if its in indeterminate state and allSelected
expect(dataTableComponent.indeterminate).toBeTruthy();
expect(dataTableComponent.allSelected).toBeTruthy();
// unselect one of the rows
- fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[2].triggerEventHandler('click', new Event('click'));
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[2].triggerEventHandler('click', new Event('click'));
fixture.detectChanges();
fixture.whenStable().then(() => {
// check to see if its in indeterminate state and not allSelected
@@ -255,8 +257,80 @@ describe('Component: DataTable', () => {
})();
});
- });
+ it('should shift click and select a range of rows',
+ (done: DoneFn) => { inject([], () => {
+ let fixture: ComponentFixture = TestBed.createComponent(TdDataTableSelectableTestComponent);
+ let element: DebugElement = fixture.debugElement;
+ let component: TdDataTableSelectableTestComponent = fixture.debugElement.componentInstance;
+
+ component.selectable = true;
+ component.multiple = true;
+ component.columns = [
+ { name: 'sku', label: 'SKU #' },
+ { name: 'item', label: 'Item name' },
+ { name: 'price', label: 'Price (US$)', numeric: true },
+ ];
+
+ component.data = [{ sku: '1452-2', item: 'Pork Chops', price: 32.11 },
+ { sku: '1421-0', item: 'Prime Rib', price: 41.15 },
+ { sku: '1452-1', item: 'Sirlone', price: 22.11 },
+ { sku: '1421-3', item: 'T-Bone', price: 51.15 }];
+
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ let dataTableComponent: TdDataTableComponent = fixture.debugElement.query(By.directive(TdDataTableComponent)).componentInstance;
+ // check how many rows (without counting the columns) were rendered
+ expect(fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent)).length - 1).toBe(4);
+ // check to see checkboxes states
+ expect(dataTableComponent.indeterminate).toBeFalsy();
+ expect(dataTableComponent.allSelected).toBeFalsy();
+
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ // select the first and last row with shift key also selected and should then select all checkboxes
+ let clickEvent: MouseEvent = document.createEvent('MouseEvents');
+ // the 12th parameter below 'true' sets the shift key to be clicked at the same time as as the mouse click
+ clickEvent.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, true/*shiftkey*/, false, 0, document.body.parentNode);
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[0].nativeElement.dispatchEvent(clickEvent);
+ fixture.debugElement.queryAll(By.directive(MdPseudoCheckbox))[3].nativeElement.dispatchEvent(clickEvent);
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ // check to see if allSelected is true
+ expect(dataTableComponent.allSelected).toBeTruthy();
+ done();
+ });
+ });
+ });
+ })();
+ });
+
+ it('should click on a row and see the rowClick Event',
+ async(inject([], () => {
+ let fixture: ComponentFixture = TestBed.createComponent(TdDataTableRowClickTestComponent);
+ let component: TdDataTableRowClickTestComponent = fixture.debugElement.componentInstance;
+
+ let eventSpy: jasmine.Spy = spyOn(component, 'clickEvent');
+
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[1].nativeElement.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(eventSpy.calls.count()).toBe(0);
+ component.clickable = true;
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[1].nativeElement.click();
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ expect(eventSpy.calls.count()).toBe(1);
+ });
+ });
+ });
+ });
+ })));
+ });
});
@Component({
@@ -266,7 +340,7 @@ describe('Component: DataTable', () => {
[columns]="columns">
`,
})
-class TdDataTableBasicComponent {
+class TdDataTableBasicTestComponent {
data: any[] = [
{ sku: '1452-2', item: 'Pork Chops', price: 32.11 },
{ sku: '1421-0', item: 'Prime Rib', price: 41.15 },
@@ -293,3 +367,28 @@ class TdDataTableSelectableTestComponent {
selectable: boolean = false;
multiple: boolean = false;
}
+
+@Component({
+ template: `
+
+ `,
+})
+class TdDataTableRowClickTestComponent {
+ data: any[] = [
+ { sku: '1452-2', item: 'Pork Chops', price: 32.11 },
+ { sku: '1421-0', item: 'Prime Rib', price: 41.15 },
+ ];
+ columns: ITdDataTableColumn[] = [
+ { name: 'sku', label: 'SKU #' },
+ { name: 'item', label: 'Item name' },
+ { name: 'price', label: 'Price (US$)', numeric: true },
+ ];
+ clickable: boolean = false;
+ clickEvent(): void {
+ /* noop */
+ }
+}
diff --git a/src/platform/core/data-table/data-table.component.ts b/src/platform/core/data-table/data-table.component.ts
index 0410ecf6a5..4598af0d2b 100644
--- a/src/platform/core/data-table/data-table.component.ts
+++ b/src/platform/core/data-table/data-table.component.ts
@@ -1,7 +1,11 @@
import { Component, Input, Output, EventEmitter, forwardRef, ChangeDetectionStrategy, ChangeDetectorRef,
- ContentChildren, TemplateRef, AfterContentInit, QueryList } from '@angular/core';
+ ContentChildren, TemplateRef, AfterContentInit, QueryList, Inject, Optional, ViewChildren } from '@angular/core';
+import { DOCUMENT } from '@angular/platform-browser';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
+import { ENTER, SPACE, UP_ARROW, DOWN_ARROW } from '@angular/material';
+
+import { TdDataTableRowComponent } from './data-table-row/data-table-row.component';
import { ITdDataTableSortChangeEvent } from './data-table-column/data-table-column.component';
import { TdDataTableTemplateDirective } from './directives/data-table-template.directive';
@@ -42,6 +46,15 @@ export interface ITdDataTableSelectAllEvent {
selected: boolean;
}
+export interface ITdDataTableRowClickEvent {
+ row: any;
+}
+
+export enum TdDataTableArrowKeyDirection {
+ Ascending = 'ASC',
+ Descending = 'DESC',
+}
+
@Component({
providers: [ TD_DATA_TABLE_CONTROL_VALUE_ACCESSOR ],
selector: 'td-data-table',
@@ -62,6 +75,7 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
private _data: any[];
private _columns: ITdDataTableColumn[];
private _selectable: boolean = false;
+ private _clickable: boolean = false;
private _multiple: boolean = true;
private _allSelected: boolean = false;
private _indeterminate: boolean = false;
@@ -71,10 +85,17 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
private _sortBy: ITdDataTableColumn;
private _sortOrder: TdDataTableSortingOrder = TdDataTableSortingOrder.Ascending;
+ /** shift select */
+ private _lastSelectedIndex: number = -1;
+ private _selectedBeforeLastIndex: number = -1;
+ private _lastArrowKeyDirection: TdDataTableArrowKeyDirection;
+
/** template fetching support */
private _templateMap: Map> = new Map>();
@ContentChildren(TdDataTableTemplateDirective) _templates: QueryList;
+ @ViewChildren(TdDataTableRowComponent) _rows: QueryList;
+
/**
* Returns true if all values are selected.
*/
@@ -163,6 +184,19 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
return this._selectable;
}
+ /**
+ * clickable?: boolean
+ * Enables row click events, hover.
+ * Defaults to 'false'
+ */
+ @Input('clickable')
+ set clickable(clickable: string | boolean) {
+ this._clickable = clickable !== '' ? (clickable === 'true' || clickable === true) : true;
+ }
+ get isClickable(): boolean {
+ return this._clickable;
+ }
+
/**
* multiple?: boolean
* Enables multiple row selection. [selectable] needs to be enabled.
@@ -247,6 +281,13 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
*/
@Output('rowSelect') onRowSelect: EventEmitter = new EventEmitter();
+ /**
+ * rowClick?: function
+ * Event emitted when a row is clicked.
+ * Emits an [ITdDataTableRowClickEvent] implemented object.
+ */
+ @Output('rowClick') onRowClick: EventEmitter = new EventEmitter();
+
/**
* selectAll?: function
* Event emitted when all rows are selected/deselected by the all checkbox. [selectable] needs to be enabled.
@@ -255,7 +296,8 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
@Output('selectAll') onSelectAll: EventEmitter =
new EventEmitter();
- constructor(private _changeDetectorRef: ChangeDetectorRef) {}
+ constructor(@Optional() @Inject(DOCUMENT) private _document: any,
+ private _changeDetectorRef: ChangeDetectorRef) {}
/**
* Loads templates and sets them in a map for faster access.
@@ -333,32 +375,70 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
}
/**
- * Selects or clears a row depending on 'checked' value
+ * Selects or clears a row depending on 'checked' value if the row 'isSelectable'
+ * handles cntrl clicks and shift clicks for multi-select
*/
- select(row: any, checked: boolean, event: Event): void {
- event.preventDefault();
- // clears all the fields for the dataset
- if (!this._multiple) {
- this.clearModel();
- }
-
- if (checked) {
- this._value.push(row);
- } else {
- // if selection is done by a [uniqueId] it uses it to compare, else it compares by reference.
- if (this.uniqueId) {
- row = this._value.filter((val: any) => {
- return val[this.uniqueId] === row[this.uniqueId];
- })[0];
+ select(row: any, event: Event, currentSelected: number): void {
+ if (this.isSelectable) {
+ this.blockEvent(event);
+ this._doSelection(row);
+
+ // Check to see if Shift key is selected and need to select everything in between
+ let mouseEvent: MouseEvent = event as MouseEvent;
+ if (this.isMultiple && mouseEvent && mouseEvent.shiftKey && this._lastSelectedIndex > -1) {
+ let firstIndex: number = currentSelected;
+ let lastIndex: number = this._lastSelectedIndex;
+ if (currentSelected > this._lastSelectedIndex) {
+ firstIndex = this._lastSelectedIndex;
+ lastIndex = currentSelected;
+ }
+ for (let i: number = firstIndex + 1; i < lastIndex; i++) {
+ this._doSelection(this._data[i]);
+ }
}
- let index: number = this._value.indexOf(row);
- if (index > -1) {
- this._value.splice(index, 1);
+ // set the last selected attribute unless the last selected unchecked a row
+ if (this.isRowSelected(this._data[currentSelected])) {
+ this._selectedBeforeLastIndex = this._lastSelectedIndex;
+ this._lastSelectedIndex = currentSelected;
+ } else {
+ this._lastSelectedIndex = this._selectedBeforeLastIndex;
+ }
+ // everything is unselected so start over
+ if (!this._indeterminate && !this._allSelected) {
+ this._lastSelectedIndex = -1;
}
}
- this._calculateCheckboxState();
- this.onRowSelect.emit({row: row, selected: checked});
- this.onChange(this._value);
+ }
+
+ /**
+ * Overrides the onselectstart method of the document so other text on the page
+ * doesn't get selected when doing shift selections.
+ */
+ disableTextSelection(): void {
+ if (this._document) {
+ this._document.onselectstart = function(): boolean {
+ return false;
+ };
+ }
+ }
+
+ /**
+ * Resets the original onselectstart method.
+ */
+ enableTextSelection(): void {
+ if (this._document) {
+ this._document.onselectstart = undefined;
+ }
+ }
+
+ /**
+ * emits the onRowClickEvent when a row is clicked
+ * if clickable is true and selectable is false then select the row
+ */
+ handleRowClick(row: any, event: Event, currentSelected: number): void {
+ if (this.isClickable) {
+ this.onRowClick.emit({row: row});
+ }
}
/**
@@ -375,6 +455,82 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
this.onSortChange.next({ name: this._sortBy.name, order: this._sortOrder });
}
+ /**
+ * Handle all keyup events when focusing a data table row
+ */
+ _rowKeyup(event: KeyboardEvent, row: any, index: number): void {
+ let length: number;
+ let rows: TdDataTableRowComponent[];
+ switch (event.keyCode) {
+ case ENTER:
+ case SPACE:
+ /** if user presses enter or space, the row should be selected */
+ this.select(row, event, index);
+ break;
+ case UP_ARROW:
+ rows = this._rows.toArray();
+ length = rows.length;
+
+ // check to see if changing direction and need to toggle the current row
+ if (this._lastArrowKeyDirection === TdDataTableArrowKeyDirection.Descending) {
+ index++;
+ }
+ /**
+ * if users presses the up arrow, we focus the prev row
+ * unless its the first row, then we move to the last row
+ */
+ if (index === 0) {
+ if (!event.shiftKey) {
+ rows[length - 1].focus();
+ }
+ } else {
+ rows[index - 1].focus();
+ }
+ this.blockEvent(event);
+ if (this.isMultiple && event.shiftKey) {
+ this._doSelection(this._data[index - 1]);
+ // if the checkboxes are all unselected then start over otherwise handle changing direction
+ this._lastArrowKeyDirection = (!this._allSelected && !this._indeterminate) ? undefined : TdDataTableArrowKeyDirection.Ascending;
+ }
+ break;
+ case DOWN_ARROW:
+ rows = this._rows.toArray();
+ length = rows.length;
+
+ // check to see if changing direction and need to toggle the current row
+ if (this._lastArrowKeyDirection === TdDataTableArrowKeyDirection.Ascending) {
+ index--;
+ }
+ /**
+ * if users presses the down arrow, we focus the next row
+ * unless its the last row, then we move to the first row
+ */
+ if (index === (length - 1)) {
+ if (!event.shiftKey) {
+ rows[0].focus();
+ }
+ } else {
+ rows[index + 1].focus();
+ }
+ this.blockEvent(event);
+ if (this.isMultiple && event.shiftKey) {
+ this._doSelection(this._data[index + 1]);
+ // if the checkboxes are all unselected then start over otherwise handle changing direction
+ this._lastArrowKeyDirection = (!this._allSelected && !this._indeterminate) ? undefined : TdDataTableArrowKeyDirection.Descending;
+ }
+ break;
+ default:
+ // default
+ }
+ }
+
+ /**
+ * Method to prevent the default events
+ */
+ blockEvent(event: Event): void {
+ event.preventDefault();
+ }
+
/**
* Implemented as part of ControlValueAccessor.
*/
@@ -405,6 +561,33 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
}
}
+ /**
+ * Does the actual Row Selection
+ */
+ private _doSelection(row: any): void {
+ let wasSelected: boolean = this.isRowSelected(row);
+ if (!this._multiple) {
+ this.clearModel();
+ }
+ if (!wasSelected) {
+ this._value.push(row);
+ } else {
+ // if selection is done by a [uniqueId] it uses it to compare, else it compares by reference.
+ if (this.uniqueId) {
+ row = this._value.filter((val: any) => {
+ return val[this.uniqueId] === row[this.uniqueId];
+ })[0];
+ }
+ let index: number = this._value.indexOf(row);
+ if (index > -1) {
+ this._value.splice(index, 1);
+ }
+ }
+ this._calculateCheckboxState();
+ this.onRowSelect.emit({row: row, selected: this.isRowSelected(row)});
+ this.onChange(this._value);
+ }
+
/**
* Calculate all the state of all checkboxes
*/
@@ -436,5 +619,4 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
}
}
}
-
}
diff --git a/src/platform/core/data-table/data-table.module.ts b/src/platform/core/data-table/data-table.module.ts
index 532e5d9e40..6897c24a14 100644
--- a/src/platform/core/data-table/data-table.module.ts
+++ b/src/platform/core/data-table/data-table.module.ts
@@ -21,7 +21,7 @@ const TD_DATA_TABLE: Type[] = [
TdDataTableTableComponent,
];
-export { TdDataTableComponent, TdDataTableSortingOrder,
+export { TdDataTableComponent, TdDataTableSortingOrder, ITdDataTableRowClickEvent,
ITdDataTableColumn, ITdDataTableSelectEvent, ITdDataTableSelectAllEvent } from './data-table.component';
export { TdDataTableService } from './services/data-table.service';
export { TdDataTableColumnComponent,
|