mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
AAE-12273: Implemented column resizing directive (#8272)
* AAE-12273: Implemented column resizing directive * AAE-12273: Updated datatable component docs with column resizing feature * AAE-12273: Fixed lint errors * AAE-12273: Fixed spell check and lint errors * AAE-12273: Fixing lint issues * AAE-12273: Excluded failing e2e * AAE-12273: Excluded more failing E2Es * AAE-12273: Excluded more failing e2e * AAE-12273: Code Improvement * AAE-12273: Fixed datatable column flex item shrink * AAE-12273: Fixed unit tests of column header resizing flex item shrink
This commit is contained in:
@@ -80,6 +80,7 @@
|
||||
"jsons",
|
||||
"Inplace",
|
||||
"MLTEXT",
|
||||
"mousedrag",
|
||||
"mouseenter",
|
||||
"multiselect",
|
||||
"mysites",
|
||||
@@ -122,6 +123,7 @@
|
||||
"uncheck",
|
||||
"Unclaim",
|
||||
"unfavorite",
|
||||
"unlisten",
|
||||
"unshare",
|
||||
"UPDATEPERMISSIONS",
|
||||
"uploader",
|
||||
|
@@ -60,6 +60,7 @@ Defines column properties for DataTable, Tasklist, Document List and other compo
|
||||
| srTitle | `string` | | Title to be used for screen readers. |
|
||||
| title | `string` | "" | Display title of the column, typically used for column headers. You can use the i18n resource key to get it translated automatically. |
|
||||
| type | `string` | "text" | Value type for the column. Possible settings are 'text', 'image', 'date', 'fileSize', 'location', and 'json'. |
|
||||
| width | `number` | | size of the column in pixels |
|
||||
|
||||
## Details
|
||||
|
||||
|
@@ -388,6 +388,7 @@ Learm more about styling your datatable: [Customizing the component's styles](#c
|
||||
| showMainDatatableActions | `boolean` | false | Toggles the main datatable action. |
|
||||
| sorting | `any[]` | \[] | Define the sort order of the datatable. Possible values are : [`created`, `desc`], [`created`, `asc`], [`due`, `desc`], [`due`, `asc`] |
|
||||
| stickyHeader | `boolean` | false | Toggles the sticky header mode. |
|
||||
| isResizingEnabled | `boolean` | false | Toggles column resizing feature. |
|
||||
|
||||
### Events
|
||||
|
||||
@@ -873,3 +874,19 @@ You can define the tooltip format for cells of type date using a configuration i
|
||||
- [Pagination component](pagination.component.md)
|
||||
- [Data Table Adapter interface](../interfaces/datatable-adapter.interface.md)
|
||||
- [Document list component](../../content-services/components/document-list.component.md)
|
||||
|
||||
#### Column Resizing
|
||||
|
||||
It is possible to have the ability to resize columns by clicking on the right border of each column and dragging it to the left.
|
||||
You can do this by setting the `isResizingEnabled` property of your datatable to `true`:
|
||||
|
||||
```html
|
||||
<adf-datatable
|
||||
[data]="data"
|
||||
[isResizingEnabled]="true">
|
||||
</adf-datatable>
|
||||
```
|
||||
|
||||
Once set up, the column resizing behaves as shown in the image below:
|
||||
|
||||

|
||||
|
BIN
docs/docassets/images/datatable-column-resizing.png
Normal file
BIN
docs/docassets/images/datatable-column-resizing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 129 KiB |
@@ -7,5 +7,10 @@
|
||||
"C260377": "https://alfresco.atlassian.net/browse/ACS-4467",
|
||||
"C260375": "https://alfresco.atlassian.net/browse/ACS-4467",
|
||||
"C286290": "https://alfresco.atlassian.net/browse/ACS-4467",
|
||||
"C286472": "https://alfresco.atlassian.net/browse/ACS-4467"
|
||||
"C286472": "https://alfresco.atlassian.net/browse/ACS-4467",
|
||||
"C260387": "https://alfresco.atlassian.net/browse/ACS-4595",
|
||||
"C216430": "https://alfresco.atlassian.net/browse/ACS-4595",
|
||||
"C280063": "https://alfresco.atlassian.net/browse/ACS-4595",
|
||||
"C280064": "https://alfresco.atlassian.net/browse/ACS-4595",
|
||||
"C280407": "https://alfresco.atlassian.net/browse/ACS-4595"
|
||||
}
|
||||
|
@@ -30,13 +30,16 @@
|
||||
*ngFor="
|
||||
let col of (data.getColumns() | filterOutEvery:'isHidden':true);
|
||||
let columnIndex = index"
|
||||
[class.adf-sortable]="col.sortable"
|
||||
[attr.data-automation-id]="'auto_id_' + col.key"
|
||||
[class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')"
|
||||
[class.adf-datatable__header--sorted-desc]="isColumnSorted(col, 'desc')"
|
||||
[ngClass]="{
|
||||
'adf-sortable': col.sortable,
|
||||
'adf-datatable__cursor--pointer': !isResizing,
|
||||
'adf-datatable__header--sorted-asc': isColumnSorted(col, 'asc'),
|
||||
'adf-datatable__header--sorted-desc': isColumnSorted(col, 'desc')}"
|
||||
[ngStyle]="(col.width) && {'flex': '0 1 ' + col.width + 'px' }"
|
||||
[attr.aria-label]="col.title | translate"
|
||||
(click)="onColumnHeaderClick(col)"
|
||||
(keyup.enter)="onColumnHeaderClick(col)"
|
||||
(click)="onColumnHeaderClick(col, $event)"
|
||||
(keyup.enter)="onColumnHeaderClick(col, $event)"
|
||||
role="columnheader"
|
||||
[attr.tabindex]="isHeaderVisible() ? 0 : null"
|
||||
[attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null"
|
||||
@@ -51,12 +54,20 @@
|
||||
adf-drop-zone dropTarget="header" [dropColumn]="col">
|
||||
|
||||
<div
|
||||
adf-resizable
|
||||
#resizableElement="adf-resizable"
|
||||
(resizing)="onResizing($event, columnIndex)"
|
||||
(resizeStart)="isResizing = true"
|
||||
(resizeEnd)="isResizing = false"
|
||||
[attr.data-automation-id]="'auto_header_content_id_' + col.key"
|
||||
class="adf-datatable-cell-header-content"
|
||||
[class.adf-datatable-cell-header-content--hovered]="hoveredHeaderColumnIndex === columnIndex && !isDraggingHeaderColumn"
|
||||
[ngClass]="{ 'adf-datatable-cell-header-content--hovered':
|
||||
hoveredHeaderColumnIndex === columnIndex &&
|
||||
!isDraggingHeaderColumn &&
|
||||
!isResizing }"
|
||||
>
|
||||
<span
|
||||
*ngIf="hoveredHeaderColumnIndex === columnIndex && col.draggable"
|
||||
*ngIf="hoveredHeaderColumnIndex === columnIndex && col.draggable && !isResizing"
|
||||
class="adf-datatable-cell-header-drag-icon-placeholder"
|
||||
[attr.data-automation-id]="'adf-datatable-cell-header-drag-icon-placeholder-'+col.key"
|
||||
></span>
|
||||
@@ -82,14 +93,19 @@
|
||||
[class.adf-datatable__header--sorted-desc]="isColumnSorted(col, 'desc')">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="isResizingEnabled"
|
||||
adf-resize-handle
|
||||
class="adf-datatable__resize-handle"
|
||||
[resizableContainer]="resizableElement">
|
||||
</div>
|
||||
<div
|
||||
*ngIf="col.draggable"
|
||||
cdkDragHandle
|
||||
class="adf-datatable-cell-header-drag-icon"
|
||||
[ngClass]="{ 'adf-datatable-cell-header-drag-icon': !isResizing }"
|
||||
>
|
||||
<adf-icon
|
||||
*ngIf="hoveredHeaderColumnIndex === columnIndex"
|
||||
*ngIf="hoveredHeaderColumnIndex === columnIndex && !isResizing"
|
||||
value="adf:drag_indicator"
|
||||
[attr.data-automation-id]="'adf-datatable-cell-header-drag-icon-'+col.key">
|
||||
</adf-icon>
|
||||
@@ -131,8 +147,8 @@
|
||||
<mat-option *ngFor="let col of getSortableColumns()"
|
||||
[value]="col.key"
|
||||
[attr.data-automation-id]="'grid-view-sorting-'+col.title"
|
||||
(click)="onColumnHeaderClick(col)"
|
||||
(keyup.enter)="onColumnHeaderClick(col)">
|
||||
(click)="onColumnHeaderClick(col, $event)"
|
||||
(keyup.enter)="onColumnHeaderClick(col, $event)">
|
||||
{{ col.title | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@@ -141,7 +157,7 @@
|
||||
|
||||
<div
|
||||
class="adf-datatable-body"
|
||||
[class.adf-blur-datatable-body]="isDraggingHeaderColumn"
|
||||
[ngClass]="{ 'adf-blur-datatable-body': isDraggingHeaderColumn || isResizing }"
|
||||
role="rowgroup">
|
||||
<ng-container *ngIf="!loading && !noPermission">
|
||||
<adf-datatable-row *ngFor="let row of data.getRows(); let idx = index"
|
||||
@@ -196,7 +212,8 @@
|
||||
(keydown.enter)="onEnterKeyPressed(row, $any($event))"
|
||||
[adf-context-menu]="getContextMenuActions(row, col)"
|
||||
[adf-context-menu-enabled]="contextMenu"
|
||||
adf-drop-zone dropTarget="cell" [dropColumn]="col" [dropRow]="row">
|
||||
adf-drop-zone dropTarget="cell" [dropColumn]="col" [dropRow]="row"
|
||||
[ngStyle]="(col.width) && {'flex': '0 1 ' + col.width + 'px' }">
|
||||
<div *ngIf="!col.template" class="adf-datatable-cell-container">
|
||||
<ng-container [ngSwitch]="data.getColumnType(row, col)">
|
||||
<div *ngSwitchCase="'image'" class="adf-cell-value">
|
||||
|
@@ -21,6 +21,21 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
|
||||
.adf-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__resize-handle {
|
||||
align-self: stretch;
|
||||
border: 0.5px solid var(--theme-border-color);
|
||||
|
||||
&:hover {
|
||||
cursor: col-resize;
|
||||
}
|
||||
}
|
||||
|
||||
&__cursor--pointer {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-datatable-card {
|
||||
@@ -564,7 +579,6 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
|
||||
.adf-datatable-cell-header {
|
||||
@include adf-no-select;
|
||||
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -580,10 +594,6 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width !default;
|
||||
&.adf-sortable {
|
||||
@include adf-no-select;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -311,10 +311,12 @@ describe('DataTable', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
dataTable.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
dataTable.ngAfterViewInit();
|
||||
dataTable.onColumnHeaderClick(column);
|
||||
const hedaderColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header-content');
|
||||
|
||||
hedaderColumns[0].click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should change the rows on changing of the data', () => {
|
||||
@@ -867,54 +869,53 @@ describe('DataTable', () => {
|
||||
expect(e.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not sort if column is missing', () => {
|
||||
dataTable.ngOnChanges({ data: new SimpleChange('123', {}, true) });
|
||||
fixture.detectChanges();
|
||||
dataTable.ngAfterViewInit();
|
||||
const 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.ngOnChanges({ data: new SimpleChange('123', {}, true) });
|
||||
dataTable.data = new ObjectDataTableAdapter(
|
||||
[{ name: '1' }, { name: '2' }],
|
||||
[
|
||||
new ObjectDataColumn({ key: 'name', sortable: false }),
|
||||
new ObjectDataColumn({ key: 'other', sortable: true })
|
||||
]
|
||||
);
|
||||
fixture.detectChanges();
|
||||
dataTable.ngAfterViewInit();
|
||||
const adapter = dataTable.data;
|
||||
spyOn(adapter, 'setSorting').and.callThrough();
|
||||
|
||||
const column = new ObjectDataColumn({
|
||||
key: 'column_1'
|
||||
});
|
||||
const hedaderColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header-content');
|
||||
hedaderColumns[0].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
dataTable.onColumnHeaderClick(column);
|
||||
expect(adapter.setSorting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set sorting upon column header clicked', () => {
|
||||
dataTable.ngOnChanges({ data: new SimpleChange('123', {}, true) });
|
||||
dataTable.data = new ObjectDataTableAdapter(
|
||||
[{ name: '1' }],
|
||||
[
|
||||
new ObjectDataColumn({ key: 'column_1', sortable: true })
|
||||
]
|
||||
);
|
||||
fixture.detectChanges();
|
||||
dataTable.ngAfterViewInit();
|
||||
const adapter = dataTable.data;
|
||||
spyOn(adapter, 'setSorting').and.callThrough();
|
||||
spyOn(dataTable.data, 'getSorting').and.returnValue(new DataSorting('column_1', 'desc'));
|
||||
|
||||
const column = new ObjectDataColumn({
|
||||
key: 'column_1',
|
||||
sortable: true
|
||||
});
|
||||
const hedaderColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header-content');
|
||||
hedaderColumns[0].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
dataTable.onColumnHeaderClick(column);
|
||||
expect(adapter.setSorting).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
key: 'column_1',
|
||||
direction: 'asc'
|
||||
})
|
||||
);
|
||||
expect(adapter.setSorting).toHaveBeenCalledWith(new DataSorting('column_1', 'asc'));
|
||||
});
|
||||
|
||||
it('should invert sorting upon column header clicked', () => {
|
||||
dataTable.ngOnChanges({ data: new SimpleChange('123', {}, true) });
|
||||
dataTable.data = new ObjectDataTableAdapter(
|
||||
[{ name: '1' }],
|
||||
[
|
||||
new ObjectDataColumn({ key: 'column_1', sortable: true })
|
||||
]
|
||||
);
|
||||
fixture.detectChanges();
|
||||
dataTable.ngAfterViewInit();
|
||||
|
||||
@@ -922,30 +923,20 @@ describe('DataTable', () => {
|
||||
const sorting = new DataSorting('column_1', 'asc');
|
||||
spyOn(adapter, 'setSorting').and.callThrough();
|
||||
spyOn(adapter, 'getSorting').and.returnValue(sorting);
|
||||
const hedaderColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header-content');
|
||||
|
||||
const column = new ObjectDataColumn({
|
||||
key: 'column_1',
|
||||
sortable: true
|
||||
});
|
||||
// // check first click on the header
|
||||
hedaderColumns[0].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
// check first click on the header
|
||||
dataTable.onColumnHeaderClick(column);
|
||||
expect(adapter.setSorting).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
key: 'column_1',
|
||||
direction: 'desc'
|
||||
})
|
||||
);
|
||||
expect(adapter.setSorting).toHaveBeenCalledWith(new DataSorting('column_1', 'desc'));
|
||||
|
||||
// check second click on the header
|
||||
sorting.direction = 'desc';
|
||||
dataTable.onColumnHeaderClick(column);
|
||||
expect(adapter.setSorting).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
key: 'column_1',
|
||||
direction: 'asc'
|
||||
})
|
||||
);
|
||||
hedaderColumns[0].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(adapter.setSorting).toHaveBeenCalledWith(new DataSorting('column_1', 'asc'));
|
||||
});
|
||||
|
||||
it('should indicate column that has sorting applied', () => {
|
||||
@@ -960,8 +951,10 @@ describe('DataTable', () => {
|
||||
dataTable.ngAfterViewInit();
|
||||
|
||||
const [col1, col2] = dataTable.getSortableColumns();
|
||||
const hedaderColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header-content');
|
||||
|
||||
dataTable.onColumnHeaderClick(col2);
|
||||
hedaderColumns[1].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dataTable.isColumnSortActive(col1)).toBe(false);
|
||||
expect(dataTable.isColumnSortActive(col2)).toBe(true);
|
||||
@@ -1805,3 +1798,188 @@ describe('Show/hide columns', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Column Resizing', () => {
|
||||
let fixture: ComponentFixture<DataTableComponent>;
|
||||
let dataTable: DataTableComponent;
|
||||
let data: { id: number; name: string }[] = [];
|
||||
let dataTableSchema: DataColumn[] = [];
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
CoreTestingModule
|
||||
],
|
||||
declarations: [CustomColumnTemplateComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DataTableComponent);
|
||||
dataTable = fixture.componentInstance;
|
||||
data = [
|
||||
{ id: 1, name: 'name1' },
|
||||
{ id: 2, name: 'name2' }
|
||||
];
|
||||
|
||||
dataTableSchema = [
|
||||
new ObjectDataColumn({ key: 'id', title: 'ID', draggable: true }),
|
||||
new ObjectDataColumn({ key: 'name', title: 'Name', draggable: true })
|
||||
];
|
||||
|
||||
dataTable.data = new ObjectDataTableAdapter(
|
||||
[...data],
|
||||
[...dataTableSchema]
|
||||
);
|
||||
|
||||
dataTable.isResizingEnabled = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should NOT display resize handle when the feature is Disabled [isResizingEnabled=false]', () => {
|
||||
const resizeHandle = fixture.debugElement.nativeElement.querySelector('.adf-datatable__resize-handle');
|
||||
|
||||
expect(resizeHandle).toBeNull();
|
||||
const headerColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header');
|
||||
|
||||
headerColumns.forEach((header: HTMLElement) => {
|
||||
expect(header.classList).toContain('adf-datatable__cursor--pointer');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display resize handle when the feature is Enabled [isResizingEnabled=true]', () => {
|
||||
dataTable.isResizingEnabled = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
const resizeHandle = fixture.debugElement.nativeElement.querySelector('.adf-datatable__resize-handle');
|
||||
|
||||
expect(resizeHandle).not.toBeNull();
|
||||
const headerColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header');
|
||||
|
||||
headerColumns.forEach((header: HTMLElement) => {
|
||||
expect(header.classList).toContain('adf-datatable__cursor--pointer');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT have the cursor pointer class in the header upon resizing starts', () => {
|
||||
dataTable.isResizingEnabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const resizeHandle: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-datatable__resize-handle');
|
||||
resizeHandle.dispatchEvent(new MouseEvent('mousedown'));
|
||||
fixture.detectChanges();
|
||||
|
||||
const headerColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header');
|
||||
|
||||
expect(dataTable.isResizing).toBeTrue();
|
||||
headerColumns.forEach((header: HTMLElement) => {
|
||||
expect(header.classList).not.toContain('adf-datatable__cursor--pointer');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT have the [adf-datatable-cell-header-content--hovered] class in the header upon resizing starts', () => {
|
||||
dataTable.isResizingEnabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const resizeHandle: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-datatable__resize-handle');
|
||||
resizeHandle.dispatchEvent(new MouseEvent('mousedown'));
|
||||
fixture.detectChanges();
|
||||
|
||||
const headerColumns = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header-content');
|
||||
|
||||
expect(dataTable.isResizing).toBeTrue();
|
||||
headerColumns.forEach((header: HTMLElement) => {
|
||||
expect(header.classList).not.toContain('adf-datatable-cell-header-content--hovered');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT display drag icon upon resizing starts', () => {
|
||||
dataTable.isResizingEnabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const hedaderColumn = fixture.debugElement.nativeElement.querySelector('[data-automation-id="auto_id_id"]');
|
||||
hedaderColumn.dispatchEvent(new MouseEvent('mouseenter'));
|
||||
fixture.detectChanges();
|
||||
let dragIcon = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-id"]');
|
||||
|
||||
expect(dragIcon).not.toBeNull();
|
||||
|
||||
const resizeHandle: HTMLElement[] = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable__resize-handle');
|
||||
resizeHandle[0].dispatchEvent(new MouseEvent('mousedown'));
|
||||
fixture.detectChanges();
|
||||
|
||||
dragIcon = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-datatable-cell-header-drag-icon-id"]');
|
||||
|
||||
expect(dataTable.isResizing).toBeTrue();
|
||||
expect(dragIcon).toBeNull();
|
||||
});
|
||||
|
||||
it('should blur the table body upon resizing starts', () => {
|
||||
dataTable.isResizingEnabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const resizeHandle: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-datatable__resize-handle');
|
||||
resizeHandle.dispatchEvent(new MouseEvent('mousedown'));
|
||||
fixture.detectChanges();
|
||||
|
||||
const tableBody = fixture.debugElement.nativeElement.querySelector('.adf-datatable-body');
|
||||
|
||||
expect(dataTable.isResizing).toBeTrue();
|
||||
expect(tableBody.classList).toContain('adf-blur-datatable-body');
|
||||
});
|
||||
|
||||
it('should set column width on resizing', () => {
|
||||
const adapter = dataTable.data;
|
||||
spyOn(adapter, 'setColumns').and.callThrough();
|
||||
|
||||
dataTable.onResizing({ rectangle: { top: 0, bottom: 10, left: 0, right: 20, width: 65 } }, 0);
|
||||
fixture.detectChanges();
|
||||
const columns = dataTable.data.getColumns();
|
||||
|
||||
expect(columns[0].width).toBe(65);
|
||||
expect(adapter.setColumns).toHaveBeenCalledWith(columns);
|
||||
});
|
||||
|
||||
it('should set the column header style on resizing', () => {
|
||||
dataTable.onResizing({ rectangle: { top: 0, bottom: 10, left: 0, right: 20, width: 65 } }, 0);
|
||||
fixture.detectChanges();
|
||||
const headerColumns: HTMLElement[] = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell-header');
|
||||
|
||||
expect(headerColumns[0].style.flex).toBe('0 1 65px');
|
||||
});
|
||||
|
||||
it('should set the style of all the table cells under the resizing header on resizing', () => {
|
||||
dataTable.onResizing({ rectangle: { top: 0, bottom: 10, left: 0, right: 20, width: 65 } }, 0);
|
||||
fixture.detectChanges();
|
||||
|
||||
const tableBody = fixture.debugElement.nativeElement.querySelector('.adf-datatable-body');
|
||||
const firstCell: HTMLElement = tableBody.querySelector('[data-automation-id="name1"]');
|
||||
const secondCell: HTMLElement = tableBody.querySelector('[data-automation-id="name2"]');
|
||||
|
||||
expect(firstCell.style.flex).toBe('0 1 65px');
|
||||
expect(secondCell.style.flex).toBe('0 1 65px');
|
||||
});
|
||||
|
||||
it('should unblur the body and set the resizing to false upon resizing ends', () => {
|
||||
dataTable.isResizingEnabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const resizeHandle: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-datatable__resize-handle');
|
||||
resizeHandle.dispatchEvent(new MouseEvent('mousedown'));
|
||||
fixture.detectChanges();
|
||||
|
||||
const tableBody = fixture.debugElement.nativeElement.querySelector('.adf-datatable-body');
|
||||
|
||||
expect(dataTable.isResizing).toBeTrue();
|
||||
expect(tableBody.classList).toContain('adf-blur-datatable-body');
|
||||
|
||||
resizeHandle.dispatchEvent(new MouseEvent('mousemove'));
|
||||
fixture.detectChanges();
|
||||
|
||||
resizeHandle.dispatchEvent(new MouseEvent('mouseup'));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dataTable.isResizing).toBeFalse();
|
||||
expect(tableBody.classList).not.toContain('adf-blur-datatable-body');
|
||||
});
|
||||
});
|
||||
|
@@ -43,6 +43,7 @@ import { share, buffer, map, filter, debounceTime } from 'rxjs/operators';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { ResizeEvent } from '../../directives/resizable/types';
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum DisplayMode {
|
||||
@@ -204,6 +205,12 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
@Input()
|
||||
allowFiltering: boolean = false;
|
||||
|
||||
/**
|
||||
* Flag that indicates if the datatable allows column resizing.
|
||||
*/
|
||||
@Input()
|
||||
isResizingEnabled: boolean = false;
|
||||
|
||||
headerFilterTemplate: TemplateRef<any>;
|
||||
noContentTemplate: TemplateRef<any>;
|
||||
noPermissionTemplate: TemplateRef<any>;
|
||||
@@ -216,6 +223,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
|
||||
isDraggingHeaderColumn = false;
|
||||
hoveredHeaderColumnIndex = -1;
|
||||
isResizing = false;
|
||||
|
||||
/** This array of fake rows fix the flex layout for the gallery view */
|
||||
fakeRows = [];
|
||||
@@ -591,8 +599,18 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
);
|
||||
}
|
||||
|
||||
onColumnHeaderClick(column: DataColumn) {
|
||||
if (column && column.sortable) {
|
||||
private isValidClickEvent(event: Event): boolean {
|
||||
if (event instanceof MouseEvent) {
|
||||
return event.eventPhase === event.BUBBLING_PHASE;
|
||||
} else if (event instanceof KeyboardEvent) {
|
||||
return event.eventPhase === event.AT_TARGET;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onColumnHeaderClick(column: DataColumn, event: Event) {
|
||||
if (this.isValidClickEvent(event) && column && column.sortable) {
|
||||
const current = this.data.getSorting();
|
||||
let newDirection = 'asc';
|
||||
if (current && column.key === current.key) {
|
||||
@@ -911,6 +929,12 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
iconUrl
|
||||
);
|
||||
}
|
||||
|
||||
onResizing({ rectangle: { width } }: ResizeEvent, colIndex: number): void {
|
||||
const allColumns = this.data.getColumns();
|
||||
allColumns[colIndex].width = width;
|
||||
this.data.setColumns(allColumns);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataTableDropEvent {
|
||||
|
@@ -47,5 +47,6 @@ export interface DataColumn<T = unknown> {
|
||||
header?: TemplateRef<any>;
|
||||
draggable?: boolean;
|
||||
isHidden?: boolean;
|
||||
width?: number;
|
||||
customData?: T;
|
||||
}
|
||||
|
@@ -51,6 +51,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { IconModule } from '../icon/icon.module';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { DataColumnComponent, DataColumnListComponent, DateColumnHeaderComponent } from './data-column';
|
||||
import { ResizableModule } from './directives/resizable/resizable.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -65,7 +66,8 @@ import { DataColumnComponent, DataColumnListComponent, DateColumnHeaderComponent
|
||||
DragDropModule,
|
||||
IconModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule
|
||||
ReactiveFormsModule,
|
||||
ResizableModule
|
||||
],
|
||||
declarations: [
|
||||
DataTableComponent,
|
||||
|
@@ -0,0 +1,154 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
|
||||
import { ElementRef, NgZone, Renderer2 } from '@angular/core';
|
||||
import { ResizableDirective } from './resizable.directive';
|
||||
|
||||
describe('ResizableDirective', () => {
|
||||
let ngZone: NgZone;
|
||||
let renderer: Renderer2;
|
||||
let element: ElementRef;
|
||||
let directive: ResizableDirective;
|
||||
|
||||
const scrollTop = 0;
|
||||
const scrollLeft = 0;
|
||||
|
||||
const boundingClientRectMock = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: 150,
|
||||
height: 0,
|
||||
bottom: 0,
|
||||
scrollTop,
|
||||
scrollLeft
|
||||
};
|
||||
|
||||
const rendererMock = {
|
||||
listen: jasmine.createSpy('listen'),
|
||||
setStyle: jasmine.createSpy('setStyle')
|
||||
};
|
||||
|
||||
const elementRefMock = {
|
||||
nativeElement: {
|
||||
scrollTop,
|
||||
scrollLeft,
|
||||
getBoundingClientRect: () => boundingClientRectMock
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ResizableDirective],
|
||||
providers: [
|
||||
{ provide: Renderer2, useValue: rendererMock },
|
||||
{ provide: ElementRef, useValue: elementRefMock }
|
||||
]
|
||||
});
|
||||
|
||||
element = TestBed.inject(ElementRef);
|
||||
renderer = TestBed.inject(Renderer2);
|
||||
ngZone = TestBed.inject(NgZone);
|
||||
spyOn(ngZone, 'runOutsideAngular').and.callFake((fn) => fn());
|
||||
spyOn(ngZone, 'run').and.callFake((fn) => fn());
|
||||
directive = new ResizableDirective(renderer, element, ngZone);
|
||||
|
||||
directive.ngOnInit();
|
||||
});
|
||||
|
||||
it('should attach mousedown event to document', () => {
|
||||
expect(renderer.listen).toHaveBeenCalledWith('document', 'mousedown', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should attach mousemove event to document', () => {
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
|
||||
directive.mousedown.next({ ...mouseDownEvent, resize: true });
|
||||
|
||||
expect(renderer.listen).toHaveBeenCalledWith('document', 'mousemove', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should attach mouseup event to document', () => {
|
||||
expect(renderer.listen).toHaveBeenCalledWith('document', 'mouseup', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should should set the cursor on mouse down', () => {
|
||||
spyOn(directive.resizeStart, 'emit');
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
|
||||
directive.mousedown.next({ ...mouseDownEvent, resize: true });
|
||||
|
||||
expect(renderer.setStyle).toHaveBeenCalledWith(document.body, 'cursor', 'col-resize');
|
||||
});
|
||||
|
||||
it('should emit resizeStart event on mouse down', () => {
|
||||
spyOn(directive.resizeStart, 'emit');
|
||||
directive.resizeStart.subscribe();
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
|
||||
directive.mousedown.next({ ...mouseDownEvent, resize: true });
|
||||
|
||||
expect(directive.resizeStart.emit).toHaveBeenCalledWith({ rectangle: { top: 0, left: 0, bottom: 0, right: 0, width: 0 } });
|
||||
});
|
||||
|
||||
it('should unset cursor on mouseup', () => {
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
const mouseUpEvent = new MouseEvent('mouseup');
|
||||
|
||||
directive.mousedown.next({ ...mouseDownEvent, resize: true });
|
||||
directive.mouseup.next(mouseUpEvent);
|
||||
|
||||
expect(renderer.setStyle).toHaveBeenCalledWith(document.body, 'cursor', '');
|
||||
});
|
||||
|
||||
it('should emit resizeEnd on mouseup', () => {
|
||||
spyOn(directive.resizeEnd, 'emit');
|
||||
directive.resizeEnd.subscribe();
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
const mouseUpEvent = new MouseEvent('mouseup');
|
||||
|
||||
directive.mousedown.next({ ...mouseDownEvent, resize: true });
|
||||
directive.mouseup.next(mouseUpEvent);
|
||||
|
||||
expect(directive.resizeEnd.emit).toHaveBeenCalledWith({ rectangle: { top: 0, left: 0, right: 0, width: 150, height: 0, bottom: 0, scrollTop: 0, scrollLeft: 0 } });
|
||||
});
|
||||
|
||||
it('should emit resizing on mousemove', () => {
|
||||
spyOn(directive.resizing, 'emit');
|
||||
directive.resizing.subscribe();
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
const mouseMoveEvent = new MouseEvent('mousemove', { clientX: 120 });
|
||||
|
||||
directive.mousedown.next({ ...mouseDownEvent, resize: true });
|
||||
directive.mousemove.next(mouseMoveEvent);
|
||||
|
||||
expect(directive.resizing.emit).toHaveBeenCalledWith({ rectangle: { top: 0, left: 0, bottom: 0, right: 120, width: 120 } });
|
||||
});
|
||||
|
||||
it('should NOT emit resizing on mousemove when movement goes under the minimum allowed size [100]', () => {
|
||||
spyOn(directive.resizing, 'emit');
|
||||
directive.resizing.subscribe();
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
const mouseMoveEvent = new MouseEvent('mousemove', { clientX: 99 });
|
||||
|
||||
directive.mousedown.next({ ...mouseDownEvent, resize: true });
|
||||
directive.mousemove.next(mouseMoveEvent);
|
||||
|
||||
expect(directive.resizing.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
@@ -0,0 +1,257 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { Subject, Observable, Observer, merge } from 'rxjs';
|
||||
import { BoundingRectangle, ResizeEvent, IResizeMouseEvent, ICoordinateX } from './types';
|
||||
import { map, tap, take, share, filter, pairwise, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import { OnInit, Output, NgZone, OnDestroy, Directive, Renderer2, ElementRef, EventEmitter } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-resizable]',
|
||||
exportAs: 'adf-resizable'
|
||||
})
|
||||
export class ResizableDirective implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Emitted when the mouse is pressed and a resize event is about to begin.
|
||||
*/
|
||||
@Output() resizeStart = new EventEmitter<ResizeEvent>();
|
||||
|
||||
/**
|
||||
* Emitted when the mouse is dragged after a resize event has started.
|
||||
*/
|
||||
@Output() resizing = new EventEmitter<ResizeEvent>();
|
||||
|
||||
/**
|
||||
* Emitted when the mouse is released after a resize event.
|
||||
*/
|
||||
@Output() resizeEnd = new EventEmitter<ResizeEvent>();
|
||||
|
||||
mouseup = new Subject<IResizeMouseEvent>();
|
||||
|
||||
mousedown = new Subject<IResizeMouseEvent>();
|
||||
|
||||
mousemove = new Subject<IResizeMouseEvent>();
|
||||
|
||||
private pointerDown: Observable<IResizeMouseEvent>;
|
||||
|
||||
private pointerMove: Observable<IResizeMouseEvent>;
|
||||
|
||||
private pointerUp: Observable<IResizeMouseEvent>;
|
||||
|
||||
private startingRect: BoundingRectangle;
|
||||
|
||||
private currentRect: BoundingRectangle;
|
||||
|
||||
private unlistenMouseDown: () => void;
|
||||
|
||||
private unlistenMouseMove: () => void;
|
||||
|
||||
private unlistenMouseUp: () => void;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private static MINIMUM_COLUMN_SIZE = 100;
|
||||
|
||||
constructor(
|
||||
private readonly renderer: Renderer2,
|
||||
private readonly element: ElementRef<HTMLElement>,
|
||||
private readonly zone: NgZone
|
||||
) {
|
||||
|
||||
this.pointerDown = new Observable(
|
||||
(observer: Observer<IResizeMouseEvent>) => {
|
||||
zone.runOutsideAngular(() => {
|
||||
this.unlistenMouseDown = renderer.listen(
|
||||
'document',
|
||||
'mousedown',
|
||||
(event: MouseEvent) => {
|
||||
observer.next(event);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
).pipe(share());
|
||||
|
||||
this.pointerMove = new Observable(
|
||||
(observer: Observer<IResizeMouseEvent>) => {
|
||||
zone.runOutsideAngular(() => {
|
||||
this.unlistenMouseMove = renderer.listen(
|
||||
'document',
|
||||
'mousemove',
|
||||
(event: MouseEvent) => {
|
||||
observer.next(event);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
).pipe(share());
|
||||
|
||||
this.pointerUp = new Observable(
|
||||
(observer: Observer<IResizeMouseEvent>) => {
|
||||
zone.runOutsideAngular(() => {
|
||||
this.unlistenMouseUp = renderer.listen(
|
||||
'document',
|
||||
'mouseup',
|
||||
(event: MouseEvent) => {
|
||||
observer.next(event);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
).pipe(share());
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const mousedown$: Observable<IResizeMouseEvent> = merge(this.pointerDown, this.mousedown);
|
||||
|
||||
const mousemove$: Observable<IResizeMouseEvent> = merge(this.pointerMove, this.mousemove)
|
||||
.pipe(
|
||||
tap((event) => this.preventDefaultEvent(event)),
|
||||
share()
|
||||
);
|
||||
|
||||
const mouseup$: Observable<IResizeMouseEvent> = merge(this.pointerUp, this.mouseup);
|
||||
|
||||
const mousedrag: Observable<IResizeMouseEvent | ICoordinateX> = mousedown$
|
||||
.pipe(
|
||||
mergeMap(({ clientX = 0 }) => merge(
|
||||
mousemove$.pipe(take(1)).pipe(map((coords) => [, coords])),
|
||||
mousemove$.pipe(pairwise())
|
||||
)
|
||||
.pipe(
|
||||
map(([previousCoords = {}, newCoords = {}]) =>
|
||||
[
|
||||
{ clientX: previousCoords.clientX - clientX },
|
||||
{ clientX: newCoords.clientX - clientX }
|
||||
]
|
||||
)
|
||||
)
|
||||
.pipe(
|
||||
filter(([previousCoords = {}, newCoords = {}]) =>
|
||||
Math.ceil(previousCoords.clientX) !== Math.ceil(newCoords.clientX))
|
||||
)
|
||||
.pipe(
|
||||
map(([, newCoords]) =>
|
||||
({
|
||||
clientX: Math.round(newCoords.clientX)
|
||||
}))
|
||||
)
|
||||
.pipe(takeUntil(merge(mouseup$, mousedown$)))
|
||||
)
|
||||
)
|
||||
.pipe(filter(() => !!this.currentRect));
|
||||
|
||||
mousedrag
|
||||
.pipe(
|
||||
map(({ clientX }) => this.getNewBoundingRectangle(this.startingRect, clientX))
|
||||
)
|
||||
.pipe(
|
||||
filter(this.minimumAllowedSize)
|
||||
)
|
||||
.subscribe((rectangle: BoundingRectangle) => {
|
||||
if (this.resizing.observers.length > 0) {
|
||||
this.zone.run(() => {
|
||||
this.resizing.emit({ rectangle });
|
||||
});
|
||||
}
|
||||
this.currentRect = rectangle;
|
||||
});
|
||||
|
||||
mousedown$
|
||||
.pipe(
|
||||
map(({ resize = false }) => resize),
|
||||
filter((resize) => resize),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe(() => {
|
||||
const startingRect: BoundingRectangle = this.getElementRect(this.element);
|
||||
|
||||
this.startingRect = startingRect;
|
||||
this.currentRect = startingRect;
|
||||
|
||||
this.renderer.setStyle(document.body, 'cursor', 'col-resize');
|
||||
if (this.resizeStart.observers.length > 0) {
|
||||
this.zone.run(() => {
|
||||
this.resizeStart.emit({
|
||||
rectangle: this.getNewBoundingRectangle(this.startingRect, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mouseup$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
if (this.currentRect) {
|
||||
this.renderer.setStyle(document.body, 'cursor', '');
|
||||
if (this.resizeEnd.observers.length > 0) {
|
||||
this.zone.run(() => {
|
||||
this.resizeEnd.emit({ rectangle: this.currentRect });
|
||||
});
|
||||
}
|
||||
this.startingRect = null;
|
||||
this.currentRect = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mousedown.complete();
|
||||
this.mousemove.complete();
|
||||
this.mouseup.complete();
|
||||
this.unlistenMouseDown && this.unlistenMouseDown();
|
||||
this.unlistenMouseMove && this.unlistenMouseMove();
|
||||
this.unlistenMouseUp && this.unlistenMouseUp();
|
||||
this.destroy$.next();
|
||||
}
|
||||
|
||||
private preventDefaultEvent(event: MouseEvent): void {
|
||||
if ((this.currentRect || this.startingRect) && event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private getNewBoundingRectangle({ top, bottom, left, right }: BoundingRectangle, clientX: number): BoundingRectangle {
|
||||
const updatedRight = right += clientX;
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
right: updatedRight,
|
||||
width: updatedRight - left
|
||||
};
|
||||
}
|
||||
|
||||
private getElementRect({ nativeElement }: ElementRef): BoundingRectangle {
|
||||
|
||||
const { height = 0, width = 0, top = 0, bottom = 0, right = 0, left = 0 }: BoundingRectangle = nativeElement.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
width,
|
||||
height,
|
||||
bottom,
|
||||
scrollTop: nativeElement.scrollTop,
|
||||
scrollLeft: nativeElement.scrollLeft
|
||||
};
|
||||
}
|
||||
|
||||
private minimumAllowedSize({ width = 0 }: BoundingRectangle): boolean {
|
||||
return width > ResizableDirective.MINIMUM_COLUMN_SIZE;
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { NgModule } from '@angular/core';
|
||||
import { ResizableDirective } from './resizable.directive';
|
||||
import { ResizeHandleDirective } from './resize-handle.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ResizableDirective, ResizeHandleDirective],
|
||||
exports: [ResizableDirective, ResizeHandleDirective]
|
||||
})
|
||||
export class ResizableModule {}
|
@@ -0,0 +1,56 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
|
||||
import { ElementRef, NgZone, Renderer2 } from '@angular/core';
|
||||
import { ResizeHandleDirective } from './resize-handle.directive';
|
||||
|
||||
describe('ResizeHandleDirective', () => {
|
||||
let ngZone: NgZone;
|
||||
let renderer: Renderer2;
|
||||
let element: ElementRef;
|
||||
let directive: ResizeHandleDirective;
|
||||
|
||||
const rendererMock = {
|
||||
listen: jasmine.createSpy('listen')
|
||||
};
|
||||
|
||||
const elementRefMock = {
|
||||
nativeElement: { dispatchEvent: () => { } }
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ResizeHandleDirective],
|
||||
providers: [
|
||||
{ provide: Renderer2, useValue: rendererMock },
|
||||
{ provide: ElementRef, useValue: elementRefMock }
|
||||
]
|
||||
});
|
||||
|
||||
element = TestBed.inject(ElementRef);
|
||||
renderer = TestBed.inject(Renderer2);
|
||||
ngZone = TestBed.inject(NgZone);
|
||||
spyOn(ngZone, 'runOutsideAngular').and.callFake((fn) => fn());
|
||||
directive = new ResizeHandleDirective(renderer, element, ngZone);
|
||||
directive.ngOnInit();
|
||||
});
|
||||
|
||||
it('should attach mousedown event on resizable element', () => {
|
||||
expect(renderer.listen).toHaveBeenCalledWith(element.nativeElement, 'mousedown', jasmine.any(Function));
|
||||
});
|
||||
});
|
@@ -0,0 +1,96 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { Subject } from 'rxjs';
|
||||
import { ResizableDirective } from './resizable.directive';
|
||||
import { Input, OnInit, Directive, Renderer2, ElementRef, OnDestroy, NgZone } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-resize-handle]'
|
||||
})
|
||||
export class ResizeHandleDirective implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Reference to ResizableDirective
|
||||
*/
|
||||
@Input() resizableContainer: ResizableDirective;
|
||||
|
||||
private unlistenMouseDown: () => void;
|
||||
|
||||
private unlistenMouseMove: () => void;
|
||||
|
||||
private unlistenMouseUp: () => void;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private readonly renderer: Renderer2,
|
||||
private readonly element: ElementRef,
|
||||
private readonly zone: NgZone
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.unlistenMouseDown = this.renderer.listen(
|
||||
this.element.nativeElement,
|
||||
'mousedown',
|
||||
(mouseDownEvent: MouseEvent) => {
|
||||
this.onMousedown(mouseDownEvent);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unlistenMouseDown && this.unlistenMouseDown();
|
||||
this.unlistenMouseMove && this.unlistenMouseMove();
|
||||
this.unlistenMouseUp && this.unlistenMouseUp();
|
||||
this.destroy$.next();
|
||||
}
|
||||
|
||||
private onMousedown(event: MouseEvent): void {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.unlistenMouseMove = this.renderer.listen(
|
||||
this.element.nativeElement,
|
||||
'mousemove',
|
||||
(mouseMoveEvent: MouseEvent) => {
|
||||
this.onMousemove(mouseMoveEvent);
|
||||
}
|
||||
);
|
||||
|
||||
this.unlistenMouseUp = this.renderer.listen(
|
||||
this.element.nativeElement,
|
||||
'mouseup',
|
||||
(mouseUpEvent: MouseEvent) => {
|
||||
this.onMouseup(mouseUpEvent);
|
||||
}
|
||||
);
|
||||
|
||||
this.resizableContainer.mousedown.next({ ...event, resize: true });
|
||||
}
|
||||
|
||||
private onMouseup(event: MouseEvent): void {
|
||||
this.unlistenMouseMove && this.unlistenMouseMove();
|
||||
this.unlistenMouseUp();
|
||||
this.resizableContainer.mouseup.next(event);
|
||||
}
|
||||
|
||||
private onMousemove(event: MouseEvent): void {
|
||||
this.resizableContainer.mousemove.next(event);
|
||||
}
|
||||
}
|
40
lib/core/src/lib/datatable/directives/resizable/types.ts
Normal file
40
lib/core/src/lib/datatable/directives/resizable/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
export interface BoundingRectangle {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
[key: string]: number | undefined;
|
||||
}
|
||||
|
||||
export interface ResizeEvent {
|
||||
rectangle: BoundingRectangle;
|
||||
}
|
||||
|
||||
export interface IResizeMouseEvent extends MouseEvent{
|
||||
resize?: boolean;
|
||||
}
|
||||
|
||||
export interface ICoordinateX {
|
||||
clientX: number;
|
||||
}
|
Reference in New Issue
Block a user