Skip to content

Commit

Permalink
feat(frozen): fix header grouping grid with frozen columns (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Oct 22, 2019
1 parent 25acddf commit 30cb09d
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 66 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"jquery-ui-dist": "^1.12.1",
"lodash.isequal": "^4.5.0",
"moment-mini": "^2.22.1",
"slickgrid": "^2.4.14",
"slickgrid": "2.4.14",
"text-encoding-utf-8": "^1.0.2"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const gridStub = {
getHeadersWidth: jest.fn(),
getHeaderColumnWidthDiff: jest.fn(),
getPreHeaderPanel: jest.fn(),
getPreHeaderPanelLeft: jest.fn(),
getPreHeaderPanelRight: jest.fn(),
getSortColumns: jest.fn(),
invalidate: jest.fn(),
onColumnsResized: new Slick.Event(),
Expand Down Expand Up @@ -78,9 +80,9 @@ describe('GroupingAndColspanService', () => {
expect(spy).toHaveBeenCalled();
});

it('should not call the "createPreHeaderRowGroupingTitle" when there are no grid options', () => {
it('should not call the "renderPreHeaderRowGroupingTitles" when there are no grid options', () => {
gridStub.getOptions = undefined;
const spy = jest.spyOn(service, 'createPreHeaderRowGroupingTitle');
const spy = jest.spyOn(service, 'renderPreHeaderRowGroupingTitles');
service.init(gridStub, dataViewStub);
expect(spy).not.toHaveBeenCalled();
});
Expand All @@ -100,64 +102,85 @@ describe('GroupingAndColspanService', () => {
jest.spyOn(gridStub, 'getPreHeaderPanel').mockReturnValue(`<div style="width: 2815px; left: -1000px;" class="slick-header-columns"></div>`);
});

it('should call the "createPreHeaderRowGroupingTitle" on initial load even when there are no column definitions', () => {
const spy = jest.spyOn(service, 'createPreHeaderRowGroupingTitle');
it('should call the "renderPreHeaderRowGroupingTitles" on initial load even when there are no column definitions', () => {
const spy = jest.spyOn(service, 'renderPreHeaderRowGroupingTitles');
gridStub.getColumns = undefined;

service.init(gridStub, dataViewStub);
jest.runAllTimers(); // fast-forward timer

expect(spy).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 50);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75);
});

it('should call the "createPreHeaderRowGroupingTitle" after triggering a grid "onSort"', () => {
const spy = jest.spyOn(service, 'createPreHeaderRowGroupingTitle');
it('should call the "renderPreHeaderRowGroupingTitles" after triggering a grid "onSort"', () => {
const spy = jest.spyOn(service, 'renderPreHeaderRowGroupingTitles');

service.init(gridStub, dataViewStub);
gridStub.onSort.notify({ impactedColumns: mockColumns }, new Slick.EventData(), gridStub);
jest.runAllTimers(); // fast-forward timer

expect(spy).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 50);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75);
});

it('should call the "createPreHeaderRowGroupingTitle" after triggering a grid "onColumnsResized"', () => {
const spy = jest.spyOn(service, 'createPreHeaderRowGroupingTitle');
it('should call the "renderPreHeaderRowGroupingTitles" after triggering a grid "onColumnsResized"', () => {
const spy = jest.spyOn(service, 'renderPreHeaderRowGroupingTitles');

service.init(gridStub, dataViewStub);
gridStub.onColumnsResized.notify({}, new Slick.EventData(), gridStub);
jest.runAllTimers(); // fast-forward timer

expect(spy).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 50);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75);
});

it('should call the "createPreHeaderRowGroupingTitle" after triggering a dataView "onColumnsResized"', () => {
const spy = jest.spyOn(service, 'createPreHeaderRowGroupingTitle');
it('should call the "renderPreHeaderRowGroupingTitles" after triggering a dataView "onColumnsResized"', () => {
const spy = jest.spyOn(service, 'renderPreHeaderRowGroupingTitles');

service.init(gridStub, dataViewStub);
dataViewStub.onRowCountChanged.notify({ previous: 1, current: 2, dataView: dataViewStub, callingOnRowsChanged: 1 }, new Slick.EventData(), gridStub);
jest.runAllTimers(); // fast-forward timer

expect(spy).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 50);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75);
});

it('should create the pre-header row grouping title DOM element', () => {
const spy = jest.spyOn(service, 'createPreHeaderRowGroupingTitle');
it('should render the pre-header row grouping title DOM element', () => {
const spy = jest.spyOn(service, 'renderPreHeaderRowGroupingTitles');
const divHeaderColumns = document.getElementsByClassName('slick-header-columns');

service.init(gridStub, dataViewStub);
jest.runAllTimers(); // fast-forward timer

expect(spy).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 50);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75);
expect(divHeaderColumns.length).toBeGreaterThan(2);
expect(divHeaderColumns[0].outerHTML).toEqual(`<div style="width: 2815px; left: -1000px;" class="slick-header-columns">All your colums div here</div>`);
});

it('should render the pre-header twice (for both left & right viewports) row grouping title DOM element', () => {
const frozenColumns = 2;
gridOptionMock.frozenColumn = frozenColumns;
const headerGroupSpy = jest.spyOn(service, 'renderHeaderGroups');
const preHeaderLeftSpy = jest.spyOn(gridStub, 'getPreHeaderPanelLeft');
const preHeaderRightSpy = jest.spyOn(gridStub, 'getPreHeaderPanelRight');
const divHeaderColumns = document.getElementsByClassName('slick-header-columns');

service.init(gridStub, dataViewStub);
jest.runAllTimers(); // fast-forward timer

expect(preHeaderLeftSpy).toHaveBeenCalledTimes(1);
expect(preHeaderRightSpy).toHaveBeenCalledTimes(1);
expect(headerGroupSpy).toHaveBeenNthCalledWith(1, expect.anything(), 0, (frozenColumns + 1));
expect(headerGroupSpy).toHaveBeenNthCalledWith(2, expect.anything(), (frozenColumns + 1), mockColumns.length);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75);
expect(divHeaderColumns.length).toBeGreaterThan(2);
expect(divHeaderColumns[0].outerHTML).toEqual(`<div style="width: 2815px; left: -1000px;" class="slick-header-columns">All your colums div here</div>`);
});
Expand Down
70 changes: 38 additions & 32 deletions src/aurelia-slickgrid/services/groupingAndColspan.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,13 @@ export class GroupingAndColspanService {
// When dealing with Pre-Header Grouping colspan, we need to re-create the pre-header in multiple occasions
// for all these events, we have to trigger a re-create
if (this._gridOptions.createPreHeaderPanel) {
this._eventHandler.subscribe(grid.onSort, (e: Event, args: any) => {
this.createPreHeaderRowGroupingTitle();
});
this._eventHandler.subscribe(grid.onColumnsResized, (e: Event, args: any) => {
this.createPreHeaderRowGroupingTitle();
});
this._eventHandler.subscribe(dataView.onRowCountChanged, (e: Event, args: any) => {
this.createPreHeaderRowGroupingTitle();
});
this._eventHandler.subscribe(grid.onSort, () => this.renderPreHeaderRowGroupingTitles());
this._eventHandler.subscribe(grid.onColumnsResized, () => this.renderPreHeaderRowGroupingTitles());
this._eventHandler.subscribe(dataView.onRowCountChanged, () => this.renderPreHeaderRowGroupingTitles());

// also not sure why at this point, but it seems that I need to call the 1st create in a delayed execution
// probably some kind of timing issues and delaying it until the grid is fully ready does help
setTimeout(() => this.createPreHeaderRowGroupingTitle(), 50);
setTimeout(() => this.renderPreHeaderRowGroupingTitles(), 75);
}
}
}
Expand All @@ -69,38 +63,50 @@ export class GroupingAndColspanService {
this._eventHandler.unsubscribeAll();
}

createPreHeaderRowGroupingTitle() {
const $preHeaderPanel = $(this._grid.getPreHeaderPanel())
.empty()
/** Create or Render the Pre-Header Row Grouping Titles */
renderPreHeaderRowGroupingTitles() {
if (this._gridOptions && this._gridOptions.frozenColumn >= 0) {
// Add column groups to left panel
let $preHeaderPanel = $(this._grid.getPreHeaderPanelLeft());
this.renderHeaderGroups($preHeaderPanel, 0, this._gridOptions.frozenColumn + 1);

// Add column groups to right panel
$preHeaderPanel = $(this._grid.getPreHeaderPanelRight());
this.renderHeaderGroups($preHeaderPanel, this._gridOptions.frozenColumn + 1, this._columnDefinitions.length);
} else {
// regular grid (not a frozen grid)
const $preHeaderPanel = $(this._grid.getPreHeaderPanel());
this.renderHeaderGroups($preHeaderPanel, 0, this._columnDefinitions.length);
}
}

renderHeaderGroups(preHeaderPanel: any, start: number, end: number) {
preHeaderPanel.empty()
.addClass('slick-header-columns')
.css('left', '-1000px')
.width(this._grid.getHeadersWidth());
preHeaderPanel.parent().addClass('slick-header');

$preHeaderPanel.parent().addClass('slick-header');
const headerColumnWidthDiff = this._grid.getHeaderColumnWidthDiff();

const headerColumnWidthDiff = this._grid.getHeaderColumnWidthDiff() || 0;
let columnDef;
let m;
let header;
let lastColumnGroup = '';
let widthTotal = 0;

for (let i = 0; i < this._columnDefinitions.length; i++) {
columnDef = this._columnDefinitions[i];
if (columnDef) {
if (lastColumnGroup === columnDef.columnGroup && i > 0) {
widthTotal += columnDef.width || 0;
if (header && header.width) {
header.width(widthTotal - headerColumnWidthDiff);
}
} else {
widthTotal = columnDef.width || 0;
header = $(`<div class="ui-state-default slick-header-column" />`)
.html(`<span class="slick-column-name">${columnDef.columnGroup || ''}</span>`)
.width((columnDef.width || 0) - headerColumnWidthDiff)
.appendTo($preHeaderPanel);
}
lastColumnGroup = columnDef.columnGroup || '';
for (let i = start; i < end; i++) {
m = this._columnDefinitions[i];
if (lastColumnGroup === m.columnGroup && i > 0) {
widthTotal += m.width;
header.width(widthTotal - headerColumnWidthDiff);
} else {
widthTotal = m.width;
header = $(`<div class="ui-state-default slick-header-column" />`)
.html(`<span class="slick-column-name">${m.columnGroup || ''}</span>`)
.width(m.width - headerColumnWidthDiff)
.appendTo(preHeaderPanel);
}
lastColumnGroup = m.columnGroup;
}
}
}
12 changes: 10 additions & 2 deletions src/examples/slickgrid/example14.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
<h2>${title}</h2>
<div class="subtitle" innerhtml.bind="subTitle"></div>

<aurelia-slickgrid grid-id="grid1" column-definitions.bind="columnDefinitions" grid-options.bind="gridOptions" dataset.bind="dataset"
grid-height="500" grid-width="800">
<h3>Grid 1 <small>(with Header Grouping &amp; Colspan)</small></h3>
<aurelia-slickgrid grid-id="grid1" column-definitions.bind="columnDefinitions1" grid-options.bind="gridOptions1"
dataset.bind="dataset1" grid-height="275" grid-width="800">
</aurelia-slickgrid>

<hr />

<h3>Grid 2 <small>(with Header Grouping &amp; Frozen/Pinned Columns)</small></h3>
<aurelia-slickgrid grid-id="grid2" column-definitions.bind="columnDefinitions2" grid-options.bind="gridOptions2"
dataset.bind="dataset2" grid-height="275" grid-width="800">
</aurelia-slickgrid>
</template>
11 changes: 11 additions & 0 deletions src/examples/slickgrid/example14.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** You can change the pinned/frozen border styling through this css override */

.slick-row .slick-cell.frozen:last-child,
.slick-headerrow-column.frozen:last-child,
.slick-footerrow-column.frozen:last-child {
border-right: 1px solid #969696 !important;
}

.slick-pane-bottom {
border-top: 1px solid #969696 !important;
}
58 changes: 44 additions & 14 deletions src/examples/slickgrid/example14.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Column, FieldType, GridOption } from '../../aurelia-slickgrid';
import './example14.scss'; // provide custom CSS/SASS styling

export class Example14 {
title = 'Example 14: Column Span & Header Grouping';
subTitle = `
This example demonstrates how to easily span a row over multiple columns & how to group header titles.
<ul>
<li>
Row Colspan - (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Row-Colspan" target="_blank">Wiki docs</a>) |
Row Colspan - (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Row-Colspan" target="_blank">Wiki docs</a>) /
Header Grouping - (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Header-Title-Grouping" target="_blank">Wiki docs</a>)
</li>
<li>Note that you can add Sort but remember that it will sort by the data that the row contains, even if the data is visually hidden by colspan it will still sort it</li>
Expand All @@ -16,22 +17,27 @@ export class Example14 {
</li>
</ul>
`;
columnDefinitions: Column[];
gridOptions: GridOption;
dataset = [];

columnDefinitions1: Column[];
columnDefinitions2: Column[];
gridOptions1: GridOption;
gridOptions2: GridOption;
dataset1 = [];
dataset2 = [];

constructor() {
// define the grid options & columns and then create the grid itself
this.defineGrid();
this.definedGrid1();
this.definedGrid2();
}

attached() {
// populate the dataset once the grid is ready
this.getData();
this.dataset1 = this.getData(500);
this.dataset2 = this.getData(500);
}

defineGrid() {
this.columnDefinitions = [
definedGrid1() {
this.columnDefinitions1 = [
{ id: 'title', name: 'Title', field: 'title', sortable: true, columnGroup: 'Common Factor' },
{ id: 'duration', name: 'Duration', field: 'duration', columnGroup: 'Common Factor' },
{ id: 'start', name: 'Start', field: 'start', columnGroup: 'Period' },
Expand All @@ -40,7 +46,7 @@ export class Example14 {
{ id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', type: FieldType.boolean, columnGroup: 'Analysis' }
];

this.gridOptions = {
this.gridOptions1 = {
enableAutoResize: false,
enableCellNavigation: true,
enableColumnReorder: false,
Expand All @@ -53,12 +59,35 @@ export class Example14 {
};
}

getData() {
definedGrid2() {
this.columnDefinitions2 = [
{ id: 'title', name: 'Title', field: 'title', sortable: true, columnGroup: 'Common Factor' },
{ id: 'duration', name: 'Duration', field: 'duration', columnGroup: 'Common Factor' },
{ id: 'start', name: 'Start', field: 'start', columnGroup: 'Period' },
{ id: 'finish', name: 'Finish', field: 'finish', columnGroup: 'Period' },
{ id: '%', name: '% Complete', field: 'percentComplete', selectable: false, columnGroup: 'Analysis' },
{ id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', type: FieldType.boolean, columnGroup: 'Analysis' }
];

this.gridOptions2 = {
alwaysShowVerticalScroll: false, // disable scroll since we don't want it to show on the left pinned columns
enableCellNavigation: true,
enableColumnReorder: false,
createPreHeaderPanel: true,
showPreHeaderPanel: true,
preHeaderPanelHeight: 25,
explicitInitialization: true,
frozenColumn: 1,
};
}

getData(count: number) {
// Set up some test columns.
this.dataset = [];
for (let i = 0; i < 500; i++) {
this.dataset[i] = {
const mockDataset = [];
for (let i = 0; i < count; i++) {
mockDataset[i] = {
id: i,
num: i,
title: 'Task ' + i,
duration: '5 days',
percentComplete: Math.round(Math.random() * 100),
Expand All @@ -67,6 +96,7 @@ export class Example14 {
effortDriven: (i % 5 === 0)
};
}
return mockDataset;
}

/**
Expand Down

0 comments on commit 30cb09d

Please sign in to comment.