Skip to content

Commit

Permalink
feat: add grid option enableHtmlRendering to use pure HTML not stri…
Browse files Browse the repository at this point in the history
…ng (#894)

* feat: add grid option `enableHtmlRendering` to use pure HTML not string
- prior to this PR, SlickGrid only used html string that are then passed to `innerHTML` but that is not CSP (Content Security Policy) friendly, what could be nice is to provide an HTMLElement directly as Formatter and other areas of the code. The `enableHtmlRendering` option is basically to disable the use of `innerHTML` within SlickGrid
- this PR is NOT complete, at this point it only adds HTMLElement to Formatter but there are still some usage of `innerHTML` in the code
  • Loading branch information
ghiscoding authored Nov 14, 2023
1 parent ff970c0 commit 448ec4f
Show file tree
Hide file tree
Showing 21 changed files with 798 additions and 98 deletions.
19 changes: 19 additions & 0 deletions cypress/e2e/example4-model-esm.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
describe('Example 4 - Model (ESM)', () => {
const GRID_ROW_HEIGHT = 25;
const titles = ['#', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven'];

beforeEach(() => {
Expand All @@ -21,6 +22,15 @@ describe('Example 4 - Model (ESM)', () => {
.each(($child, index) => expect($child.text()).to.eq(titles[index]));
});

it('should expect first row to include "Task 0" and other specific properties', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 0');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '5 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) .percent-complete-bar`).should('exist');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '01/01/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', '01/05/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).find('.sgi.sgi-checkbox-intermediate').should('have.length', 1);
});

it('should display the text "Showing all 50000 rows" without Pagination', () => {
const expectedRows = ['Task 0', 'Task 1', 'Task 2', 'Task 3', 'Task 4'];

Expand Down Expand Up @@ -116,4 +126,13 @@ describe('Example 4 - Model (ESM)', () => {
expect(win.console.log).to.be.calledWith('on After Paging Info Changed - New Paging:: ', { pageSize: 50, pageNum: 999, totalRows: 50000, totalPages: 1000 });
});
});

it('should expect first row to include "Task 49950" and other specific properties', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 49950');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '5 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) .percent-complete-bar`).should('exist');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '01/01/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', '01/05/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).find('.sgi.sgi-checkbox-intermediate').should('have.length', 1);
});
});
138 changes: 138 additions & 0 deletions cypress/e2e/example4-model-html-formatters.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
describe('Example 4 - HTML Formatters', () => {
const GRID_ROW_HEIGHT = 25;
const titles = ['#', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven'];

beforeEach(() => {
// create a console.log spy for later use
cy.window().then((win) => {
cy.spy(win.console, "log");
});
});

it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/examples/example4-model-html-formatters.html`);
cy.get('h2').contains('Demonstrates');
cy.get('h2 + ul > li').first().contains('a filtered Model (DataView) as a data source instead of a simple array');
});

it('should have exact Column Titles in the grid', () => {
cy.get('#myGrid')
.find('.slick-header-columns')
.children()
.each(($child, index) => expect($child.text()).to.eq(titles[index]));
});

it('should expect first row to include "Task 0" and other specific properties', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 0');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '5 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) .percent-complete-bar`).should('exist');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '01/01/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', '01/05/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).find('.sgi.sgi-checkbox-intermediate').should('have.length', 1);
});

it('should display the text "Showing all 50000 rows" without Pagination', () => {
const expectedRows = ['Task 0', 'Task 1', 'Task 2', 'Task 3', 'Task 4'];

cy.get('.slick-pager-status')
.contains('Showing all 50000 rows');

cy.get('#myGrid')
.find('.slick-row')
.each(($row, index) => {
if (index > expectedRows.length - 1) {
return;
}
cy.wrap($row).children('.slick-cell:nth(1)')
.first()
.should('contain', expectedRows[index]);
});
});

it('Should display "Showing page 1 of 1000" text after changing Pagination to 50 items per page', () => {
cy.get('.sgi-lightbulb')
.click();

cy.get('.slick-pager-settings-expanded')
.should('be.visible');

cy.get('.slick-pager-settings-expanded')
.contains('50')
.click();

cy.get('.slick-pager-status')
.contains('Showing page 1 of 1000');

cy.window().then((win) => {
expect(win.console.log).to.have.callCount(2);
expect(win.console.log).to.be.calledWith('on Before Paging Info Changed - Previous Paging:: ', { pageSize: 0, pageNum: 0, totalRows: 50000, totalPages: 1 });
expect(win.console.log).to.be.calledWith('on After Paging Info Changed - New Paging:: ', { pageSize: 50, pageNum: 0, totalRows: 50000, totalPages: 1000 });
});
});

it('Should display "Showing page 2 of 1000" text after clicking on next page', () => {
const expectedRows = ['Task 50', 'Task 51', 'Task 52', 'Task 53', 'Task 54'];

cy.get('.sgi-chevron-start.sgi-state-disabled');
cy.get('.sgi-chevron-left.sgi-state-disabled');

cy.get('.sgi-chevron-right')
.click();

cy.get('.slick-pager-status')
.contains('Showing page 2 of 1000');

cy.get('#myGrid')
.find('.slick-row')
.each(($row, index) => {
if (index > expectedRows.length - 1) {
return;
}
cy.wrap($row).children('.slick-cell:nth(1)')
.first()
.should('contain', expectedRows[index]);
});

cy.window().then((win) => {
expect(win.console.log).to.have.callCount(2);
expect(win.console.log).to.be.calledWith('on Before Paging Info Changed - Previous Paging:: ', { pageSize: 50, pageNum: 0, totalRows: 50000, totalPages: 1000 });
expect(win.console.log).to.be.calledWith('on After Paging Info Changed - New Paging:: ', { pageSize: 50, pageNum: 1, totalRows: 50000, totalPages: 1000 });
});
});

it('Should display "Showing page 1000 of 1000" text after clicking on last page', () => {
const expectedRows = ['Task 49950', 'Task 49951', 'Task 49952', 'Task 49953', 'Task 49954'];

cy.get('.sgi-chevron-end')
.click();

cy.get('.slick-pager-status')
.contains('Showing page 1000 of 1000');

cy.get('#myGrid')
.find('.slick-row')
.each(($row, index) => {
if (index > expectedRows.length - 1) {
return;
}
cy.wrap($row).children('.slick-cell:nth(1)')
.first()
.should('contain', expectedRows[index]);
});

cy.window().then((win) => {
expect(win.console.log).to.have.callCount(2);
expect(win.console.log).to.be.calledWith('on Before Paging Info Changed - Previous Paging:: ', { pageSize: 50, pageNum: 1, totalRows: 50000, totalPages: 1000 });
expect(win.console.log).to.be.calledWith('on After Paging Info Changed - New Paging:: ', { pageSize: 50, pageNum: 999, totalRows: 50000, totalPages: 1000 });
});
});

it('should expect first row to include "Task 49950" and other specific properties', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 49950');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '5 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) .percent-complete-bar`).should('exist');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '01/01/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', '01/05/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).find('.sgi.sgi-checkbox-intermediate').should('have.length', 1);
});
});
19 changes: 19 additions & 0 deletions cypress/e2e/example4-model.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
describe('Example 4 - Model', () => {
const GRID_ROW_HEIGHT = 25;
const titles = ['#', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven'];

beforeEach(() => {
Expand All @@ -21,6 +22,15 @@ describe('Example 4 - Model', () => {
.each(($child, index) => expect($child.text()).to.eq(titles[index]));
});

it('should expect first row to include "Task 0" and other specific properties', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 0');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '5 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) .percent-complete-bar`).should('exist');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '01/01/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', '01/05/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).find('.sgi.sgi-checkbox-intermediate').should('have.length', 1);
});

it('should display the text "Showing all 50000 rows" without Pagination', () => {
const expectedRows = ['Task 0', 'Task 1', 'Task 2', 'Task 3', 'Task 4'];

Expand Down Expand Up @@ -116,4 +126,13 @@ describe('Example 4 - Model', () => {
expect(win.console.log).to.be.calledWith('on After Paging Info Changed - New Paging:: ', { pageSize: 50, pageNum: 999, totalRows: 50000, totalPages: 1000 });
});
});

it('should expect first row to include "Task 49950" and other specific properties', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 49950');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '5 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) .percent-complete-bar`).should('exist');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '01/01/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', '01/05/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).find('.sgi.sgi-checkbox-intermediate').should('have.length', 1);
});
});
10 changes: 8 additions & 2 deletions examples/example-plugin-custom-tooltip.html
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,10 @@ <h2>View Source:</h2>
function tooltipFormatter(row, cell, value, column, dataContext) {
const tooltipTitle = 'Custom Tooltip';
const effortDrivenHtml = Slick.Formatters.Checkmark(row, cell, dataContext.effortDriven, column, dataContext);
const completionBarHtml = Slick.Formatters.PercentCompleteBar(row, cell, dataContext.percentComplete, column, dataContext);
let completionBarHtml = Slick.Formatters.PercentCompleteBar(row, cell, dataContext.percentComplete, column, dataContext);
if (completionBarHtml instanceof HTMLElement) {
completionBarHtml = completionBarHtml.outerHTML;
}
return '<div class="bold">' + tooltipTitle + '</div>'
+ '<div class="tooltip-2cols-row"><div>Id:</div> <div>' + dataContext.id + '</div></div>'
+ '<div class="tooltip-2cols-row"><div>Title:</div> <div>' + dataContext.title + '</div></div>'
Expand All @@ -313,7 +316,10 @@ <h2>View Source:</h2>

// use a 2nd Formatter to get the percent completion
// any properies provided from the `asyncPost` will end up in the `__params` property (unless a different prop name is provided via `asyncParamsPropName`)
const completionBar = Slick.Formatters.PercentCompleteBar(row, cell, dataContext.percentComplete, column, dataContext);
let completionBar = Slick.Formatters.PercentCompleteBar(row, cell, dataContext.percentComplete, column, dataContext);
if (completionBar instanceof HTMLElement) {
completionBar = completionBar.outerHTML;
}
const out = '<div class="bold">' + tooltipTitle + '</div>'
+ '<div class="tooltip-2cols-row"><div>Completion:</div> <div>' + completionBar + '</div></div>'
+ '<div class="tooltip-2cols-row"><div>Lifespan:</div> <div>' + dataContext.__params.lifespan.toFixed(2) + '</div></div>'
Expand Down
Loading

0 comments on commit 448ec4f

Please sign in to comment.