From e722456d015b2f0abdc4b6a5315683aa54a96291 Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Mon, 30 Oct 2023 14:22:40 -0400 Subject: [PATCH] chore: add CSP header example, backported from PR #884 (#891) * chore: add CSP header example - ref issue #878 * chore: add DOMPurify to CSP example * fix: move `innerHTML` as separate assignment outside of createDomElement - to further improve CSP support for issue #878, we need to move `innerHTML` as separate assignment and not use it directly within a `createDomElement`, so for example this line `const elm = createDomElement('div', { innerHTML: '' })` should be split in 2 lines `const elm = createDomElement('div'); elm.innerHTML = '';` --- cypress/integration/example-colspan.spec.js | 1 + .../integration/example-csp-header.spec.js | 52 ++++++++++++++++ examples/example-csp-header.html | 44 +++++++++++++ examples/example-csp-header.js | 61 +++++++++++++++++++ examples/index.html | 13 ++-- slick.grid.js | 15 +++-- 6 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 cypress/integration/example-csp-header.spec.js create mode 100644 examples/example-csp-header.html create mode 100644 examples/example-csp-header.js diff --git a/cypress/integration/example-colspan.spec.js b/cypress/integration/example-colspan.spec.js index 1421cbb00..b9a40ef43 100644 --- a/cypress/integration/example-colspan.spec.js +++ b/cypress/integration/example-colspan.spec.js @@ -10,6 +10,7 @@ describe('Example - Column Span & Header Grouping', { retries: 1 }, () => { it('should display Example title', () => { cy.visit(`${Cypress.config('baseExampleUrl')}/example-colspan.html`); cy.get('h2').contains('Demonstrates'); + cy.get('h2 + ul > li').first().contains('column span'); }); it('should have exact column titles', () => { diff --git a/cypress/integration/example-csp-header.spec.js b/cypress/integration/example-csp-header.spec.js new file mode 100644 index 000000000..e0a5d4700 --- /dev/null +++ b/cypress/integration/example-csp-header.spec.js @@ -0,0 +1,52 @@ +describe('Example CSP Header - with Column Span & Header Grouping', () => { + // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows + const GRID_ROW_HEIGHT = 25; + const fullTitles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven']; + for (let i = 0; i < 30; i++) { + fullTitles.push(`Mock${i}`); + } + + 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('baseExampleUrl')}/example-csp-header.html`); + cy.get('h2').contains('Demonstrates'); + cy.get('h2 + ul > li').first().contains('column span'); + }); + + it('should have exact column titles', () => { + cy.get('#myGrid') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should expect 1st row to be 1 column spanned to the entire width', () => { + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(1)`).should('not.exist'); + }); + + it('should expect 2nd row to be 4 columns and not be spanned', () => { + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(1)`).should('contain', '5 days'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(2)`).should('contain', '01/05/2009'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(3)`).contains(/(true|false)/); + }); + + it('should expect 3rd row to be 1 column spanned to the entire width', () => { + cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0)`).should('contain', 'Task 2'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('not.exist'); + }); + + it('should expect 4th row to be 4 columns and not be spanned', () => { + cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(0)`).should('contain', 'Task 3'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', '5 days'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(2)`).should('contain', '01/05/2009'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(3)`).contains(/(true|false)/); + }); +}); diff --git a/examples/example-csp-header.html b/examples/example-csp-header.html new file mode 100644 index 000000000..021ba2dd6 --- /dev/null +++ b/examples/example-csp-header.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + +
+
+
+

+ + Demonstrates: +

+
    +
  • column span
  • +
+

View Source:

+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/example-csp-header.js b/examples/example-csp-header.js new file mode 100644 index 000000000..5a8d07818 --- /dev/null +++ b/examples/example-csp-header.js @@ -0,0 +1,61 @@ +document.addEventListener("DOMContentLoaded", function () { + var grid; + var columns = [ + { id: "title", name: "Title", field: "title" }, + { id: "duration", name: "Duration", field: "duration" }, + { id: "%", name: "% Complete", field: "percentComplete", selectable: false, width: 100 }, + { id: "start", name: "Start", field: "start" }, + { id: "finish", name: "Finish", field: "finish" }, + { id: "effort-driven", name: "Effort Driven", field: "effortDriven", width: 100 } + ]; + + var options = { + enableCellNavigation: true, + enableColumnReorder: false, + sanitizer: (html) => window.DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true }) + }; + let gridElement = document.getElementById("myGrid"); + gridElement.style.width = "600px"; + gridElement.style.height = "500px"; + + let linkElement = document.getElementById("link"); + //text-decoration: none; font-size: 22px + linkElement.style.textDecoration = "none"; + linkElement.style.fontSize = "22px"; + + var data = []; + for (var i = 0; i < 500; i++) { + data[i] = { + title: "Task " + i, + duration: "5 days", + percentComplete: Math.round(Math.random() * 100), + start: "01/01/2009", + finish: "01/05/2009", + effortDriven: (i % 5 === 0) + }; + } + + data.getItemMetadata = function (row) { + if (row % 2 === 1) { + return { + "columns": { + "duration": { + "colspan": 3 + } + } + }; + } else { + return { + "columns": { + 0: { + "colspan": "*" + } + } + }; + } + }; + + grid = new Slick.Grid("#myGrid", data, columns, options); + + grid.setSelectionModel(new Slick.CellSelectionModel()); +}); \ No newline at end of file diff --git a/examples/index.html b/examples/index.html index 67ecaf522..7267edeae 100644 --- a/examples/index.html +++ b/examples/index.html @@ -79,10 +79,15 @@

Grouping

Other Features

Bootstrap, Dynamic Grids and Third Party component editors

diff --git a/slick.grid.js b/slick.grid.js index 2da292f19..feab78e61 100644 --- a/slick.grid.js +++ b/slick.grid.js @@ -1287,7 +1287,8 @@ if (typeof Slick === "undefined") { const headerRowTarget = hasFrozenColumns() ? ((i <= options.frozenColumn) ? _headerRowL : _headerRowR) : _headerRowL; const header = utils.createDomElement('div', { id: `${uid + m.id}`, dataset: { id: m.id }, className: 'ui-state-default slick-header-column', title: m.toolTip || '' }, headerTarget); - utils.createDomElement('span', { className: 'slick-column-name', innerHTML: sanitizeHtmlString(m.name) }, header); + const colNameElm = utils.createDomElement('span', { className: 'slick-column-name' }, header); + colNameElm.innerHTML = sanitizeHtmlString(m.name); utils.width(header, m.width - headerColumnWidthDiff); let classname = m.headerCssClass || null; @@ -2743,7 +2744,8 @@ if (typeof Slick === "undefined") { // headers have not yet been created, create a new node let header = getHeader(columnDef); headerColEl = utils.createDomElement('div', { id: dummyHeaderColElId, className: 'ui-state-default slick-header-column', }, header); - utils.createDomElement('span', { className: 'slick-column-name', innerHTML: sanitizeHtmlString(columnDef.name) }, headerColEl); + const colNameElm = utils.createDomElement('span', { className: 'slick-column-name' }, headerColEl); + colNameElm.innerHTML = sanitizeHtmlString(columnDef.name); clone.style.cssText = 'position: absolute; visibility: hidden;right: auto;text-overflow: initial;white-space: nowrap;'; if (columnDef.headerCssClass) { headerColEl.classList.add(...columnDef.headerCssClass.split(' ')); @@ -4125,7 +4127,8 @@ if (typeof Slick === "undefined") { return; } - var x = utils.createDomElement('div', { innerHTML: sanitizeHtmlString(stringArray.join('')) }); + var x = document.createElement('div'); + x.innerHTML = sanitizeHtmlString(stringArray.join('')); var processedRow; var node; while ((processedRow = processedRows.pop()) != null) { @@ -4185,8 +4188,10 @@ if (typeof Slick === "undefined") { if (!rows.length) { return; } - let x = utils.createDomElement('div', { innerHTML: sanitizeHtmlString(stringArrayL.join('')) }); - let xRight = utils.createDomElement('div', { innerHTML: sanitizeHtmlString(stringArrayR.join('')) }); + const x = document.createElement('div'); + const xRight = document.createElement('div'); + x.innerHTML = sanitizeHtmlString(stringArrayL.join('')); + xRight.innerHTML = sanitizeHtmlString(stringArrayR.join('')); for (var i = 0, ii = rows.length; i < ii; i++) { if (( hasFrozenRows ) && ( rows[i] >= actualFrozenRow )) {