[ADF-667] selection mode and row styles (#1914)

* selection mode and row styles

- single/multiple/none selection modes for DataTable component (and Document List)
- support for custom row styles (inline and classname values)
- fix karma config (material themes)
- readme updates
- package-lock.json files for NPM5 support
- updated DataTable demo to demonstrate selection modes and row styles

* remove package lock files
This commit is contained in:
Denys Vuika
2017-05-31 17:48:47 +01:00
committed by Eugenio Romano
parent 950a987a6c
commit 5025303980
15 changed files with 225 additions and 37 deletions

View File

@@ -17,7 +17,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { MdSlideToggleModule, MdInputModule } from '@angular/material'; import { MdSlideToggleModule, MdInputModule, MdSelectModule } from '@angular/material';
import { CoreModule } from 'ng2-alfresco-core'; import { CoreModule } from 'ng2-alfresco-core';
import { SearchModule } from 'ng2-alfresco-search'; import { SearchModule } from 'ng2-alfresco-search';
@@ -64,6 +64,7 @@ import {
routing, routing,
MdInputModule, MdInputModule,
MdSlideToggleModule, MdSlideToggleModule,
MdSelectModule,
CoreModule.forRoot(), CoreModule.forRoot(),
LoginModule.forRoot(), LoginModule.forRoot(),
SearchModule.forRoot(), SearchModule.forRoot(),

View File

@@ -0,0 +1,10 @@
alfresco-datatable >>> .custom-row-style.alfresco-datatable__row:focus {
outline-offset: -1px;
outline-width: 1px;
outline-color: green;
outline-style: solid;
}
alfresco-datatable >>> .custom-row-style.alfresco-datatable__row--selected {
color: green;
}

View File

@@ -1,8 +1,10 @@
<div class="p-10"> <div class="p-10">
<alfresco-datatable <alfresco-datatable
[data]="data" [data]="data"
[selectionMode]="selectionMode"
[multiselect]="multiselect" [multiselect]="multiselect"
[actions]="true" [actions]="true"
rowStyleClass="custom-row-style"
(showRowActionsMenu)="onShowRowActionsMenu($event)" (showRowActionsMenu)="onShowRowActionsMenu($event)"
(executeRowAction)="onExecuteRowAction($event)" (executeRowAction)="onExecuteRowAction($event)"
(row-click)="onRowClick($event)" (row-click)="onRowClick($event)"
@@ -22,6 +24,14 @@
<div class="p-10"> <div class="p-10">
<md-checkbox [(ngModel)]="multiselect">Multiselect</md-checkbox> <md-checkbox [(ngModel)]="multiselect">Multiselect</md-checkbox>
</div> </div>
<div class="p-10">
<p>For 'Multiple' selection mode use Cmd (macOS) or Ctrl (Win) to toggle selection of multiple items.</p>
<md-select placeholder="Selection Mode" [(ngModel)]="selectionMode" name="food">
<md-option *ngFor="let mode of selectionModes" [value]="mode.value">
{{mode.viewValue}}
</md-option>
</md-select>
</div>
<div class="p-10"> <div class="p-10">
<button md-raised-button (click)="reset()">Reset to default</button> <button md-raised-button (click)="reset()">Reset to default</button>
<button md-raised-button (click)="addRow()">Add row</button> <button md-raised-button (click)="addRow()">Add row</button>

View File

@@ -15,18 +15,28 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ObjectDataTableAdapter, DataSorting, ObjectDataRow, ObjectDataColumn, DataCellEvent, DataRowActionEvent } from 'ng2-alfresco-datatable'; import { ObjectDataTableAdapter, DataSorting, ObjectDataRow, ObjectDataColumn, DataCellEvent, DataRowActionEvent } from 'ng2-alfresco-datatable';
@Component({ @Component({
selector: 'datatable-demo', selector: 'datatable-demo',
templateUrl: './datatable-demo.component.html' templateUrl: './datatable-demo.component.html',
styleUrls: ['./datatable-demo.component.css']
}) })
export class DataTableDemoComponent { export class DataTableDemoComponent {
multiselect: boolean = false; multiselect: boolean = false;
data: ObjectDataTableAdapter; data: ObjectDataTableAdapter;
@Input()
selectionMode = 'single';
selectionModes = [
{ value: 'none', viewValue: 'None' },
{ value: 'single', viewValue: 'Single' },
{ value: 'multiple', viewValue: 'Multiple' }
];
private _imageUrl: string = 'http://placehold.it/140x100'; private _imageUrl: string = 'http://placehold.it/140x100';
private _createdBy: any = { private _createdBy: any = {
name: 'Denys Vuika', name: 'Denys Vuika',

View File

@@ -32,20 +32,20 @@
<!-- Example #1: using custom template with implicit access to data context --> <!-- Example #1: using custom template with implicit access to data context -->
<!-- <!--
<template let-entry="$implicit"> <ng-template let-entry="$implicit">
<span>Hi! {{entry.data.getValue(entry.row, entry.col)}}</span> <span>Hi! {{entry.data.getValue(entry.row, entry.col)}}</span>
</template> </ng-template>
--> -->
<!-- Example #2: using custom template with value access --> <!-- Example #2: using custom template with value access -->
<!-- <!--
<template let-value="value"> <ng-template let-value="value">
<span>Hi! {{value}}</span> <span>Hi! {{value}}</span>
</template> </ng-template>
--> -->
</data-column> </data-column>
<!-- Notes: has performance problems due to multiple files/folders causing separate HTTP calls to get tags --> <!-- Notes: has performance overhead due to multiple files/folders causing separate HTTP calls to get tags -->
<!-- <!--
<data-column <data-column
title="{{'DOCUMENT_LIST.COLUMNS.TAG' | translate}}" title="{{'DOCUMENT_LIST.COLUMNS.TAG' | translate}}"

View File

@@ -8,6 +8,7 @@ module.exports = function (config) {
files: [ files: [
{pattern: './node_modules/hammerjs/hammer.min.js', included: true, watched: false}, {pattern: './node_modules/hammerjs/hammer.min.js', included: true, watched: false},
{pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', included: true, watched: false},
//diagrams //diagrams
{pattern: './node_modules/chart.js/dist/Chart.js', included: true, watched: false}, {pattern: './node_modules/chart.js/dist/Chart.js', included: true, watched: false},

View File

@@ -227,6 +227,9 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `selectionMode` | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. |
| `rowStyle` | string | | The inline style to apply to every row, see [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples |
| `rowStyleClass` | string | | The CSS class to apply to every row |
| `data` | DataTableAdapter | instance of **ObjectDataTableAdapter** | data source | | `data` | DataTableAdapter | instance of **ObjectDataTableAdapter** | data source |
| `rows` | Object[] | [] | The rows that the datatable should show | | `rows` | Object[] | [] | The rows that the datatable should show |
| `multiselect` | boolean | false | Toggles multiple row selection, renders checkboxes at the beginning of each row | | `multiselect` | boolean | false | Toggles multiple row selection, renders checkboxes at the beginning of each row |

View File

@@ -109,11 +109,15 @@
} }
.alfresco-datatable__row:focus { .alfresco-datatable__row:focus {
outline-offset: -4px; outline-offset: -1px;
outline-width: 1px;
outline-color: rgb(68,138,255);
outline-style: solid;
} }
.alfresco-datatable__row--selected { .alfresco-datatable__row--selected,
color: rgb(68,138,255); .alfresco-datatable__row--selected:hover {
background-color: #e0e0e0;
} }
.adf-upload__dragging > td { .adf-upload__dragging > td {

View File

@@ -31,8 +31,10 @@
<tr *ngFor="let row of data.getRows(); let idx = index" tabindex="0" <tr *ngFor="let row of data.getRows(); let idx = index" tabindex="0"
class="alfresco-datatable__row" class="alfresco-datatable__row"
[class.alfresco-datatable__row--selected]="selectedRow === row" [class.alfresco-datatable__row--selected]="row.isSelected"
[adf-upload]="allowDropFiles && rowAllowsDrop(row)" [adf-upload-data]="row"> [adf-upload]="allowDropFiles && rowAllowsDrop(row)" [adf-upload-data]="row"
[ngStyle]="rowStyle"
[ngClass]="rowStyleClass">
<!-- Actions (left) --> <!-- Actions (left) -->
<td *ngIf="actions && actionsPosition === 'left'" class="alfresco-datatable__actions-cell"> <td *ngIf="actions && actionsPosition === 'left'" class="alfresco-datatable__actions-cell">

View File

@@ -18,7 +18,7 @@
import { SimpleChange } from '@angular/core'; import { SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core'; import { CoreModule } from 'ng2-alfresco-core';
import { MdCheckboxChange } from '@angular/material'; import { MdCheckboxModule, MdCheckboxChange } from '@angular/material';
import { DataTableComponent } from './datatable.component'; import { DataTableComponent } from './datatable.component';
import { DataTableCellComponent } from './datatable-cell.component'; import { DataTableCellComponent } from './datatable-cell.component';
import { import {
@@ -26,7 +26,7 @@ import {
DataColumn, DataColumn,
DataSorting, DataSorting,
ObjectDataTableAdapter, ObjectDataTableAdapter,
ObjectDataColumn ObjectDataColumn, ObjectDataRow
} from './../../data/index'; } from './../../data/index';
describe('DataTable', () => { describe('DataTable', () => {
@@ -39,7 +39,8 @@ describe('DataTable', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CoreModule.forRoot() CoreModule.forRoot(),
MdCheckboxModule
], ],
declarations: [ declarations: [
DataTableCellComponent, DataTableCellComponent,
@@ -52,7 +53,6 @@ describe('DataTable', () => {
fixture = TestBed.createComponent(DataTableComponent); fixture = TestBed.createComponent(DataTableComponent);
dataTable = fixture.componentInstance; dataTable = fixture.componentInstance;
element = fixture.debugElement.nativeElement; element = fixture.debugElement.nativeElement;
//fixture.detectChanges();
}); });
beforeEach(() => { beforeEach(() => {
@@ -66,6 +66,93 @@ describe('DataTable', () => {
}; };
}); });
it('should reset selection on mode change', () => {
spyOn(dataTable, 'resetSelection').and.callThrough();
dataTable.data = new ObjectDataTableAdapter(
[
{ name: '1' },
{ name: '2' }
],
[ new ObjectDataColumn({ key: 'name'}) ]
);
const rows = dataTable.data.getRows();
rows[0].isSelected = true;
rows[1].isSelected = true;
expect(rows[0].isSelected).toBeTruthy();
expect(rows[1].isSelected).toBeTruthy();
dataTable.ngOnChanges({
selectionMode: new SimpleChange(null, 'multiple', false)
});
expect(dataTable.resetSelection).toHaveBeenCalled();
});
it('should select only one row with [single] selection mode', () => {
dataTable.selectionMode = 'single';
dataTable.data = new ObjectDataTableAdapter(
[
{ name: '1' },
{ name: '2' }
],
[ new ObjectDataColumn({ key: 'name'}) ]
);
const rows = dataTable.data.getRows();
dataTable.onRowClick(rows[0], null);
expect(rows[0].isSelected).toBeTruthy();
expect(rows[1].isSelected).toBeFalsy();
dataTable.onRowClick(rows[1], null);
expect(rows[0].isSelected).toBeFalsy();
expect(rows[1].isSelected).toBeTruthy();
});
it('should unselect the row with [single] selection mode', () => {
dataTable.selectionMode = 'single';
dataTable.data = new ObjectDataTableAdapter(
[
{ name: '1' },
{ name: '2' }
],
[ new ObjectDataColumn({ key: 'name'}) ]
);
const rows = dataTable.data.getRows();
dataTable.onRowClick(rows[0], null);
expect(rows[0].isSelected).toBeTruthy();
expect(rows[1].isSelected).toBeFalsy();
dataTable.onRowClick(rows[0], null);
expect(rows[0].isSelected).toBeFalsy();
expect(rows[1].isSelected).toBeFalsy();
});
it('should select multiple rows with [multiple] selection mode', () => {
dataTable.selectionMode = 'multiple';
dataTable.data = new ObjectDataTableAdapter(
[
{ name: '1' },
{ name: '2' }
],
[ new ObjectDataColumn({ key: 'name'}) ]
);
const rows = dataTable.data.getRows();
const event = new MouseEvent('click', {
metaKey: true
});
dataTable.onRowClick(rows[0], event);
dataTable.onRowClick(rows[1], event);
expect(rows[0].isSelected).toBeTruthy();
expect(rows[1].isSelected).toBeTruthy();
});
it('should put actions menu to the right by default', () => { it('should put actions menu to the right by default', () => {
dataTable.data = new ObjectDataTableAdapter([], [ dataTable.data = new ObjectDataTableAdapter([], [
<DataColumn> {}, <DataColumn> {},

View File

@@ -52,6 +52,9 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
@Input() @Input()
rows: any[] = []; rows: any[] = [];
@Input()
selectionMode: string = 'single'; // none|single|multiple
@Input() @Input()
multiselect: boolean = false; multiselect: boolean = false;
@@ -70,6 +73,12 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
@Input() @Input()
allowDropFiles: boolean = false; allowDropFiles: boolean = false;
@Input()
rowStyle: string;
@Input()
rowStyleClass: string;
@Output() @Output()
rowClick: EventEmitter<DataRowEvent> = new EventEmitter<DataRowEvent>(); rowClick: EventEmitter<DataRowEvent> = new EventEmitter<DataRowEvent>();
@@ -88,10 +97,6 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
noContentTemplate: TemplateRef<any>; noContentTemplate: TemplateRef<any>;
isSelectAllChecked: boolean = false; isSelectAllChecked: boolean = false;
get selectedRow(): DataRow {
return this.data.selectedRow;
}
constructor(@Optional() private el: ElementRef) { constructor(@Optional() private el: ElementRef) {
} }
@@ -111,6 +116,10 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
} }
return; return;
} }
if (changes.selectionMode && !changes.selectionMode.isFirstChange()) {
this.resetSelection();
}
} }
isPropertyChanged(property: SimpleChange): boolean { isPropertyChanged(property: SimpleChange): boolean {
@@ -146,25 +155,50 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
} }
} }
onRowClick(row: DataRow, e?: Event) { onRowClick(row: DataRow, e: MouseEvent) {
if (e) { if (e) {
e.preventDefault(); e.preventDefault();
} }
if (this.data) { if (row) {
this.data.selectedRow = row; if (this.data) {
const newValue = !row.isSelected;
const rows = this.data.getRows();
if (this.isSingleSelectionMode()) {
rows.forEach(r => r.isSelected = false);
row.isSelected = newValue;
}
if (this.isMultiSelectionMode()) {
const modifier = e.metaKey || e.ctrlKey;
if (!modifier) {
rows.forEach(r => r.isSelected = false);
}
row.isSelected = newValue;
}
}
let event = new DataRowEvent(row, e, this);
this.rowClick.emit(event);
if (!event.defaultPrevented && this.el.nativeElement) {
this.el.nativeElement.dispatchEvent(
new CustomEvent('row-click', {
detail: event,
bubbles: true
})
);
}
} }
}
let event = new DataRowEvent(row, e, this); resetSelection(): void {
this.rowClick.emit(event); if (this.data) {
const rows = this.data.getRows();
if (!event.defaultPrevented && this.el.nativeElement) { if (rows && rows.length > 0) {
this.el.nativeElement.dispatchEvent( rows.forEach(r => r.isSelected = false);
new CustomEvent('row-click', { }
detail: event,
bubbles: true
})
);
} }
} }
@@ -268,4 +302,16 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
rowAllowsDrop(row: DataRow): boolean { rowAllowsDrop(row: DataRow): boolean {
return row.isDropTarget === true; return row.isDropTarget === true;
} }
hasSelectionMode(): boolean {
return this.isSingleSelectionMode() || this.isMultiSelectionMode();
}
isSingleSelectionMode(): boolean {
return this.selectionMode && this.selectionMode.toLowerCase() === 'single';
}
isMultiSelectionMode(): boolean {
return this.selectionMode && this.selectionMode.toLowerCase() === 'multiple';
}
} }

View File

@@ -196,12 +196,11 @@ export class ObjectDataTableAdapter implements DataTableAdapter {
// Simple implementation of the DataRow interface. // Simple implementation of the DataRow interface.
export class ObjectDataRow implements DataRow { export class ObjectDataRow implements DataRow {
isSelected: boolean = false; constructor(private obj: any, public isSelected: boolean = false) {
constructor(private obj: any) {
if (!obj) { if (!obj) {
throw new Error('Object source not found'); throw new Error('Object source not found');
} }
} }
getValue(key: string): any { getValue(key: string): any {

View File

@@ -176,6 +176,9 @@ The properties currentFolderId, folderNode and node are the entry initialization
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `selectionMode` | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. |
| `rowStyle` | string | | The inline style to apply to every row, see [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples |
| `rowStyleClass` | string | | The CSS class to apply to every row |
| `currentFolderId` | string | null | Initial node ID of displayed folder. Can be `-root-`, `-shared-`, `-my-`, or a fixed node ID | | `currentFolderId` | string | null | Initial node ID of displayed folder. Can be `-root-`, `-shared-`, `-my-`, or a fixed node ID |
| `folderNode` | `MinimalNodeEntryEntity` | null | Currently displayed folder node | | `folderNode` | `MinimalNodeEntryEntity` | null | Currently displayed folder node |
| `node` | `NodePaging` | null | Document list will show all the node contained in the NodePaging entity | | `node` | `NodePaging` | null | Document list will show all the node contained in the NodePaging entity |

View File

@@ -6,6 +6,7 @@
(permissionErrorEvent)="onPermissionError($event)"> (permissionErrorEvent)="onPermissionError($event)">
</alfresco-document-menu-action> </alfresco-document-menu-action>
<alfresco-datatable <alfresco-datatable
[selectionMode]="selectionMode"
[data]="data" [data]="data"
[actions]="contentActions" [actions]="contentActions"
[actionsPosition]="contentActionsPosition" [actionsPosition]="contentActionsPosition"
@@ -13,6 +14,8 @@
[fallbackThumbnail]="fallbackThumbnail" [fallbackThumbnail]="fallbackThumbnail"
[allowDropFiles]="allowDropFiles" [allowDropFiles]="allowDropFiles"
[contextMenu]="contextMenuActions" [contextMenu]="contextMenuActions"
[rowStyle]="rowStyle"
[rowStyleClass]="rowStyleClass"
(showRowContextMenu)="onShowRowContextMenu($event)" (showRowContextMenu)="onShowRowContextMenu($event)"
(showRowActionsMenu)="onShowRowActionsMenu($event)" (showRowActionsMenu)="onShowRowActionsMenu($event)"
(executeRowAction)="onExecuteRowAction($event)" (executeRowAction)="onExecuteRowAction($event)"

View File

@@ -63,6 +63,9 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
@Input() @Input()
thumbnails: boolean = false; thumbnails: boolean = false;
@Input()
selectionMode: string = 'single'; // null|single|multiple
@Input() @Input()
multiselect: boolean = false; multiselect: boolean = false;
@@ -93,6 +96,12 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
@Input() @Input()
sorting: string[]; sorting: string[];
@Input()
rowStyle: string;
@Input()
rowStyleClass: string;
skipCount: number = 0; skipCount: number = 0;
pagination: Pagination; pagination: Pagination;