Move 'pagination' from 'core' to 'datatable'

This commit is contained in:
Denys Vuika
2016-07-14 14:50:47 +01:00
parent 4c01dd4d34
commit e5355bd603
16 changed files with 75 additions and 27 deletions

View File

@@ -0,0 +1,107 @@
:host .full-width { width: 100%; }
:host .icon-cell {
font-size: 24px;
cursor: default;
}
:host .image-cell {
width: 24px;
height: 24px;
cursor: default;
}
:host .data-cell {
cursor: default;
}
:host .cell-value {}
:host .column-header {
cursor: pointer;
user-select: none;
-webkit-user-select: none; /* Chrome/Safari/Opera */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
-webkit-touch-callout: none; /* iOS Safari */
}
/* Empty folder */
:host .no-content-container {
padding: 0 !important;
}
:host .no-content-container > img {
width: 100%;
}
:host .ellipsis-cell > div
{
position: relative;
overflow: hidden;
/*height: 1em;*/
}
/* visible content */
:host .ellipsis-cell > div > span
{
display: block;
position: absolute;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1em; /* for vertical align of text */
}
/* cell stretching content */
:host .ellipsis-cell > div:after
{
content: attr(title);
overflow: hidden;
height: 0;
display: block;
}
/* Utils */
:host .non-selectable {
user-select: none;
-webkit-user-select: none; /* Chrome/Safari/Opera */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
-webkit-touch-callout: none; /* iOS Safari */
}
:host .sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
/* small desktop */
@media all and (max-width: 1200px) {}
/* tablet */
@media all and (max-width: 1024px) {}
/* mobile phone */
@media all and (max-width: 768px) {
.desktop-only {
display: none;
}
}
@media (max-device-width: 768px){
.desktop-only {
display: none;
}
}

View File

@@ -0,0 +1,91 @@
<table
*ngIf="data"
class="mdl-data-table mdl-js-data-table mdl-shadow--2dp full-width">
<thead>
<tr>
<!-- Columns -->
<th *ngIf="multiselect">
<label
class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect mdl-data-table__select"
[class.is-checked]="isSelectAllChecked"
for="table-header"
(click)="onSelectAllClick($event)">
<input type="checkbox" id="table-header" class="mdl-checkbox__input" />
</label>
</th>
<th class="mdl-data-table__cell--non-numeric non-selectable {{col.cssClass}}"
*ngFor="let col of data.getColumns()"
[attr.data-automation-id]="'auto_id_' + col.key"
[class.column-header]="col.title"
[class.mdl-data-table__header--sorted-ascending]="isColumnSorted(col, 'asc')"
[class.mdl-data-table__header--sorted-descending]="isColumnSorted(col, 'desc')"
(click)="onColumnHeaderClick(col)">
<span *ngIf="col.srTitle" class="sr-only">{{col.srTitle}}</span>
<span *ngIf="col.title">{{col.title}}</span>
</th>
<!-- Actions -->
<th *ngIf="actions">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data.getRows(); let idx = index">
<td *ngIf="multiselect">
<label
class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect mdl-data-table__select"
[attr.for]="'row[' + idx + ']'"
[class.is-checked]="row.isSelected">
<input type="checkbox" [attr.id]="'row[' + idx + ']'" class="mdl-checkbox__input" [(ngModel)]="row.isSelected" />
</label>
</td>
<td *ngFor="let col of data.getColumns()" [ngSwitch]="col.type"
class="mdl-data-table__cell--non-numeric non-selectable data-cell {{col.cssClass}}"
(click)="onRowClick(row, $event)"
(dblclick)="onRowDblClick(row, $event)"
[context-menu]="getContextMenuActions(row, col)">
<div *ngSwitchCase="'image'" class="cell-value">
<i *ngIf="isIconValue(row, col)" class="material-icons icon-cell">{{asIconValue(row, col)}}</i>
<img *ngIf="!isIconValue(row, col)" class="image-cell" alt="{{iconAltTextKey(data.getValue(row, col))|translate}}" src="{{data.getValue(row, col)}}">
</div>
<div *ngSwitchCase="'date'" class="cell-value">
{{data.getValue(row, col)}}
</div>
<div *ngSwitchCase="'text'" class="cell-value">
{{data.getValue(row, col)}}
</div>
<span *ngSwitchDefault class="cell-value">
<!-- empty cell for unknown column type -->
</span>
</td>
<td *ngIf="actions">
<!-- action menu -->
<button [id]="'action_menu_' + idx" class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">more_vert</i>
</button>
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect"
[attr.for]="'action_menu_' + idx">
<li class="mdl-menu__item"
[attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)">
{{action.title}}
</li>
</ul>
</td>
</tr>
<tr *ngIf="data.getRows().length === 0">
<td class="mdl-data-table__cell--non-numeric no-content-container"
[attr.colspan]="1 + data.getColumns().length">
<template *ngIf="noContentTemplate"
ngFor [ngForOf]="[data]"
[ngForTemplate]="noContentTemplate">
</template>
</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,323 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
it,
describe,
expect,
beforeEach
} from '@angular/core/testing';
import { DataTableComponent } from './datatable.component';
import {
DataRow,
DataColumn,
DataSorting,
ObjectDataTableAdapter,
ObjectDataColumn
} from './../../data/index';
describe('DataTable', () => {
let dataTable: DataTableComponent;
let eventMock: any;
beforeEach(() => {
// reset MDL handler
window['componentHandler'] = null;
dataTable = new DataTableComponent();
eventMock = {
preventDefault: function () {}
};
});
it('should initialize default adapter', () => {
expect(dataTable.data).toBeUndefined();
dataTable.ngOnInit();
expect(dataTable.data).toEqual(jasmine.any(ObjectDataTableAdapter));
});
it('should initialize with custom data', () => {
let data = new ObjectDataTableAdapter([], []);
dataTable.data = data;
dataTable.ngOnInit();
expect(dataTable.data).toBe(data);
});
it('should emit row click event', done => {
let row = <DataRow> {};
dataTable.rowClick.subscribe(e => {
expect(e.value).toBe(row);
done();
});
dataTable.onRowClick(row, null);
});
it('should emit row double-click event', done => {
let row = <DataRow> {};
dataTable.rowDblClick.subscribe(e => {
expect(e.value).toBe(row);
done();
});
dataTable.onRowDblClick(row, null);
});
it('should prevent default behaviour on row click event', () => {
let e = jasmine.createSpyObj('event', ['preventDefault']);
dataTable.ngOnInit();
dataTable.onRowClick(null, e);
expect(e.preventDefault).toHaveBeenCalled();
});
it('should prevent default behaviour on row double-click event', () => {
let e = jasmine.createSpyObj('event', ['preventDefault']);
dataTable.ngOnInit();
dataTable.onRowDblClick(null, e);
expect(e.preventDefault).toHaveBeenCalled();
});
it('should prevent default behaviour on select all click', () => {
let e = jasmine.createSpyObj('event', ['preventDefault']);
dataTable.onSelectAllClick(e);
expect(e.preventDefault).toHaveBeenCalled();
});
it('should not sort if column is missing', () => {
dataTable.ngOnInit();
let adapter = dataTable.data;
spyOn(adapter, 'setSorting').and.callThrough();
dataTable.onColumnHeaderClick(null);
expect(adapter.setSorting).not.toHaveBeenCalled();
});
it('should not sort upon clicking non-sortable column header', () => {
dataTable.ngOnInit();
let adapter = dataTable.data;
spyOn(adapter, 'setSorting').and.callThrough();
let column = new ObjectDataColumn({
key: 'column_1'
});
dataTable.onColumnHeaderClick(column);
expect(adapter.setSorting).not.toHaveBeenCalled();
});
it('should set sorting upon column header clicked', () => {
dataTable.ngOnInit();
let adapter = dataTable.data;
spyOn(adapter, 'setSorting').and.callThrough();
let column = new ObjectDataColumn({
key: 'column_1',
sortable: true
});
dataTable.onColumnHeaderClick(column);
expect(adapter.setSorting).toHaveBeenCalledWith(
jasmine.objectContaining({
key: 'column_1',
direction: 'asc'
})
);
});
it('should invert sorting upon column header clicked', () => {
dataTable.ngOnInit();
let adapter = dataTable.data;
let sorting = new DataSorting('column_1', 'asc');
spyOn(adapter, 'setSorting').and.callThrough();
spyOn(adapter, 'getSorting').and.returnValue(sorting);
let column = new ObjectDataColumn({
key: 'column_1',
sortable: true
});
// check first click on the header
dataTable.onColumnHeaderClick(column);
expect(adapter.setSorting).toHaveBeenCalledWith(
jasmine.objectContaining({
key: 'column_1',
direction: 'desc'
})
);
// check second click on the header
sorting.direction = 'desc';
dataTable.onColumnHeaderClick(column);
expect(adapter.setSorting).toHaveBeenCalledWith(
jasmine.objectContaining({
key: 'column_1',
direction: 'asc'
})
);
});
it('should upgrade MDL components on view checked', () => {
let handler = jasmine.createSpyObj('componentHandler', ['upgradeAllRegistered']);
window['componentHandler'] = handler;
dataTable.ngAfterViewChecked();
expect(handler.upgradeAllRegistered).toHaveBeenCalled();
});
it('should upgrade MDL components only when component handler present', () => {
expect(window['componentHandler']).toBeNull();
dataTable.ngAfterViewChecked();
});
it('should invert "select all" status', () => {
expect(dataTable.isSelectAllChecked).toBeFalsy();
dataTable.onSelectAllClick(null);
expect(dataTable.isSelectAllChecked).toBeTruthy();
dataTable.onSelectAllClick(null);
expect(dataTable.isSelectAllChecked).toBeFalsy();
});
it('should update rows on "select all" click', () => {
let data = new ObjectDataTableAdapter([{}, {}, {}], []);
let rows = data.getRows();
dataTable.data = data;
dataTable.multiselect = true;
dataTable.ngOnInit();
dataTable.onSelectAllClick(null);
expect(dataTable.isSelectAllChecked).toBe(true);
for (let i = 0; i < rows.length; i++) {
expect(rows[i].isSelected).toBe(true);
}
dataTable.onSelectAllClick(null);
expect(dataTable.isSelectAllChecked).toBe(false);
for (let i = 0; i < rows.length; i++) {
expect(rows[i].isSelected).toBe(false);
}
});
it('should allow "select all" calls with no rows', () => {
dataTable.multiselect = true;
dataTable.ngOnInit();
dataTable.onSelectAllClick(null);
expect(dataTable.isSelectAllChecked).toBe(true);
});
it('should require multiselect option to toggle row state', () => {
let data = new ObjectDataTableAdapter([{}, {}, {}], []);
let rows = data.getRows();
dataTable.data = data;
dataTable.multiselect = false;
dataTable.ngOnInit();
dataTable.onSelectAllClick(null);
expect(dataTable.isSelectAllChecked).toBe(true);
for (let i = 0; i < rows.length; i++) {
expect(rows[i].isSelected).toBe(false);
}
});
it('should require row and column for icon value check', () => {
expect(dataTable.isIconValue(null, null)).toBeFalsy();
expect(dataTable.isIconValue(<DataRow> {}, null)).toBeFalsy();
expect(dataTable.isIconValue(null, <DataColumn> {})).toBeFalsy();
});
it('should use special material url scheme', () => {
let column = <DataColumn> {};
let row = {
getValue: function (key: string) {
return 'material-icons://android';
}
};
expect(dataTable.isIconValue(<DataRow> row, column)).toBeTruthy();
});
it('should not use special material url scheme', () => {
let column = <DataColumn> {};
let row = {
getValue: function (key: string) {
return 'http://www.google.com';
}
};
expect(dataTable.isIconValue(<DataRow> row, column)).toBeFalsy();
});
it('should parse icon value', () => {
let column = <DataColumn> {};
let row = {
getValue: function (key: string) {
return 'material-icons://android';
}
};
expect(dataTable.asIconValue(<DataRow> row, column)).toBe('android');
});
it('should not parse icon value', () => {
let column = <DataColumn> {};
let row = {
getValue: function (key: string) {
return 'http://www.google.com';
}
};
expect(dataTable.asIconValue(<DataRow> row, column)).toBe(null);
});
it('should parse icon values to a valid i18n key', () => {
expect(dataTable.iconAltTextKey('custom')).toBe('ICONS.custom');
expect(dataTable.iconAltTextKey('/path/to/custom')).toBe('ICONS.custom');
expect(dataTable.iconAltTextKey('/path/to/custom.svg')).toBe('ICONS.custom');
});
it('should require column and direction to evaluate sorting state', () => {
expect(dataTable.isColumnSorted(null, null)).toBeFalsy();
expect(dataTable.isColumnSorted(<DataColumn> {}, null)).toBeFalsy();
expect(dataTable.isColumnSorted(null, 'asc')).toBeFalsy();
});
it('should require adapter sorting to evaluate sorting state', () => {
dataTable.ngOnInit();
spyOn(dataTable.data, 'getSorting').and.returnValue(null);
expect(dataTable.isColumnSorted(<DataColumn> {}, 'asc')).toBeFalsy();
});
it('should evaluate column sorting state', () => {
dataTable.ngOnInit();
spyOn(dataTable.data, 'getSorting').and.returnValue(new DataSorting('column_1', 'asc'));
expect(dataTable.isColumnSorted(<DataColumn> {key: 'column_1'}, 'asc')).toBeTruthy();
expect(dataTable.isColumnSorted(<DataColumn> {key: 'column_2'}, 'desc')).toBeFalsy();
});
});

View File

@@ -0,0 +1,201 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Component,
// NgZone,
OnInit,
Input,
Output,
EventEmitter,
AfterViewChecked,
TemplateRef
} from '@angular/core';
import {
CONTEXT_MENU_DIRECTIVES,
AlfrescoPipeTranslate
} from 'ng2-alfresco-core';
import {
DataTableAdapter,
DataRow,
DataColumn,
DataSorting,
DataRowEvent,
ObjectDataTableAdapter
} from '../../data/index';
declare var componentHandler;
declare let __moduleName: string;
@Component({
moduleId: __moduleName,
selector: 'alfresco-datatable',
styleUrls: ['./datatable.component.css'],
templateUrl: './datatable.component.html',
directives: [CONTEXT_MENU_DIRECTIVES],
pipes: [AlfrescoPipeTranslate]
})
export class DataTableComponent implements OnInit, AfterViewChecked {
@Input()
data: DataTableAdapter;
@Input()
multiselect: boolean = false;
@Input()
actions: boolean = false;
@Output()
rowClick: EventEmitter<DataRowEvent> = new EventEmitter<DataRowEvent>();
@Output()
rowDblClick: EventEmitter<DataRowEvent> = new EventEmitter<DataRowEvent>();
noContentTemplate: TemplateRef<any>;
isSelectAllChecked: boolean = false;
@Output()
showRowContextMenu: EventEmitter<any> = new EventEmitter();
@Output()
showRowActionsMenu: EventEmitter<any> = new EventEmitter();
@Output()
executeRowAction: EventEmitter<any> = new EventEmitter();
// TODO: left for reference, will be removed during future revisions
constructor(/*private _ngZone?: NgZone*/) {
}
ngOnInit() {
if (!this.data) {
this.data = new ObjectDataTableAdapter([], []);
}
}
ngAfterViewChecked() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
onRowClick(row: DataRow, e?: Event) {
if (e) {
e.preventDefault();
}
this.rowClick.emit({
value: row,
event: e
});
}
onRowDblClick(row: DataRow, e?: Event) {
if (e) {
e.preventDefault();
}
this.rowDblClick.emit({
value: row,
event: e
});
}
onColumnHeaderClick(column: DataColumn) {
if (column && column.sortable) {
let current = this.data.getSorting();
let newDirection = 'asc';
if (current && column.key === current.key) {
newDirection = current.direction === 'asc' ? 'desc' : 'asc';
}
this.data.setSorting(new DataSorting(column.key, newDirection));
}
}
onSelectAllClick(e?: Event) {
if (e) {
e.preventDefault();
}
this.isSelectAllChecked = !this.isSelectAllChecked;
if (this.multiselect) {
let rows = this.data.getRows();
if (rows && rows.length > 0) {
for (let i = 0; i < rows.length; i++) {
rows[i].isSelected = this.isSelectAllChecked;
}
// TODO: left for reference, will be removed during future revisions
/*
this._ngZone.run(() => {
this.data.getRows()[1].isSelected = true;
});
*/
}
}
}
isIconValue(row: DataRow, col: DataColumn) {
if (row && col) {
let value = row.getValue(col.key);
return value && value.startsWith('material-icons://');
}
return false;
}
asIconValue(row: DataRow, col: DataColumn) {
if (this.isIconValue(row, col)) {
let value = row.getValue(col.key) || '';
return value.replace('material-icons://', '');
}
return null;
}
iconAltTextKey(value: string) {
return 'ICONS.' + value.substring(value.lastIndexOf('/') + 1).replace(/\.[a-z]+/, '');
}
isColumnSorted(col: DataColumn, direction: string) {
if (col && direction) {
let sorting = this.data.getSorting();
return sorting && sorting.key === col.key && sorting.direction === direction;
}
return false;
}
getContextMenuActions(row: DataRow, col: DataColumn) {
let args = { row: row, col: col, actions: [] };
this.showRowContextMenu.emit({ args: args });
return args.actions;
}
getRowActions(row: DataRow, col: DataColumn) {
let args = { row: row, col: col, actions: [] };
this.showRowActionsMenu.emit({ args: args });
return args.actions;
}
onExecuteRowAction(row: DataRow, action: any) {
let args = { row: row, action: action };
this.executeRowAction.emit({ args: args });
}
}

View File

@@ -0,0 +1,27 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DataTableComponent } from './datatable.component';
import { NoContentTemplateComponent } from './no-content-template.component';
export * from './datatable.component';
export * from './no-content-template.component';
export const ALFRESCO_DATATABLE_DIRECTIVES: [any] = [
DataTableComponent,
NoContentTemplateComponent
];

View File

@@ -0,0 +1,41 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Directive,
ContentChild,
TemplateRef,
AfterContentInit
} from '@angular/core';
import { DataTableComponent } from './datatable.component';
@Directive({
selector: 'no-content-template'
})
export class NoContentTemplateComponent implements AfterContentInit {
@ContentChild(TemplateRef)
template: any;
constructor(
private dataTable: DataTableComponent) {
}
ngAfterContentInit() {
this.dataTable.noContentTemplate = this.template;
}
}