diff --git a/cypress/e2e/example-colspan.cy.ts b/cypress/e2e/example-colspan.cy.ts index 85aebc02..2b2fbf11 100644 --- a/cypress/e2e/example-colspan.cy.ts +++ b/cypress/e2e/example-colspan.cy.ts @@ -7,8 +7,9 @@ describe('Example - Column Span & Header Grouping', { retries: 1 }, () => { } it('should display Example title', () => { - cy.visit(`${Cypress.config('baseUrl')}/examples/example-colspan.html`); - cy.get('h2').contains('Demonstrates'); + cy.visit(`${Cypress.config('baseUrl')}/examples/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/e2e/example-csp-header.cy.ts b/cypress/e2e/example-csp-header.cy.ts new file mode 100644 index 00000000..4c2bcc66 --- /dev/null +++ b/cypress/e2e/example-csp-header.cy.ts @@ -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('baseUrl')}/examples/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 index c125e19b..f5c0d991 100644 --- a/examples/example-csp-header.html +++ b/examples/example-csp-header.html @@ -4,7 +4,8 @@ - + + @@ -30,6 +31,7 @@

View Source:

+ diff --git a/examples/example-csp-header.js b/examples/example-csp-header.js index 504ab559..ddcd200a 100644 --- a/examples/example-csp-header.js +++ b/examples/example-csp-header.js @@ -11,7 +11,7 @@ var columns = [ var options = { enableCellNavigation: true, enableColumnReorder: false, - sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { RETURN_TRUSTED_TYPE: true }) + sanitizer: (html) => DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true }) }; document.addEventListener("DOMContentLoaded", function () { diff --git a/examples/example-csp-policy.js b/examples/example-csp-policy.js new file mode 100644 index 00000000..5d9d6f70 --- /dev/null +++ b/examples/example-csp-policy.js @@ -0,0 +1,5 @@ +if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing + trustedTypes.createPolicy('sanitizeWithDomPurify', { + createHTML: string => DOMPurify.sanitize(string, { RETURN_TRUSTED_TYPE: true }) + }); +} \ No newline at end of file diff --git a/src/slick.core.ts b/src/slick.core.ts index f538d7bd..59548bed 100644 --- a/src/slick.core.ts +++ b/src/slick.core.ts @@ -726,6 +726,11 @@ export class Utils { if (elementOptions) { Object.keys(elementOptions).forEach((elmOptionKey) => { + if (elmOptionKey === 'innerHTML') { + console.warn(`[SlickGrid] For better CSP (Content Security Policy) support, do not use "innerHTML" directly in "createDomElement('${tagName}', { innerHTML: 'some html'})"` + + `, it is better as separate assignment: "const elm = createDomElement('span'); elm.innerHTML = 'some html';"`); + } + const elmValue = elementOptions[elmOptionKey as keyof typeof elementOptions]; if (typeof elmValue === 'object') { Object.assign(elm[elmOptionKey as K] as object, elmValue); diff --git a/src/slick.grid.ts b/src/slick.grid.ts index fd0727d9..891d7d23 100644 --- a/src/slick.grid.ts +++ b/src/slick.grid.ts @@ -1537,7 +1537,9 @@ export class SlickGrid = Column, O e const headerRowTarget = this.hasFrozenColumns() ? ((i <= this._options.frozenColumn!) ? this._headerRowL : this._headerRowR) : this._headerRowL; const header = Utils.createDomElement('div', { id: `${this.uid + m.id}`, dataset: { id: String(m.id) }, className: 'ui-state-default slick-state-default slick-header-column', title: m.toolTip || '' }, headerTarget); - Utils.createDomElement('span', { className: 'slick-column-name', innerHTML: this.sanitizeHtmlString(m.name as string) }, header); + const colNameElm = Utils.createDomElement('span', { className: 'slick-column-name' }, header); + colNameElm.innerHTML = this.sanitizeHtmlString(m.name as string); + Utils.width(header, m.width! - this.headerColumnWidthDiff); let classname = m.headerCssClass || null; @@ -3017,7 +3019,8 @@ export class SlickGrid = Column, O e // headers have not yet been created, create a new node const header = this.getHeader(columnDef) as HTMLElement; headerColEl = Utils.createDomElement('div', { id: dummyHeaderColElId, className: 'ui-state-default slick-state-default slick-header-column' }, header); - Utils.createDomElement('span', { className: 'slick-column-name', innerHTML: this.sanitizeHtmlString(String(columnDef.name)) }, headerColEl); + const colNameElm = Utils.createDomElement('span', { className: 'slick-column-name' }, headerColEl); + colNameElm.innerHTML = this.sanitizeHtmlString(String(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(' ')); @@ -4552,7 +4555,9 @@ export class SlickGrid = Column, O e return; } - const x = Utils.createDomElement('div', { innerHTML: this.sanitizeHtmlString(stringArray.join('')) }); + const x = document.createElement('div'); + x.innerHTML = this.sanitizeHtmlString(stringArray.join('')); + let processedRow: number | null | undefined; let node: HTMLElement; while (Utils.isDefined(processedRow = processedRows.pop())) { @@ -4612,8 +4617,10 @@ export class SlickGrid = Column, O e if (!rows.length) { return; } - const x = Utils.createDomElement('div', { innerHTML: this.sanitizeHtmlString(stringArrayL.join('')) }); - const xRight = Utils.createDomElement('div', { innerHTML: this.sanitizeHtmlString(stringArrayR.join('')) }); + const x = document.createElement('div'); + const xRight = document.createElement('div'); + x.innerHTML = this.sanitizeHtmlString(stringArrayL.join('')); + xRight.innerHTML = this.sanitizeHtmlString(stringArrayR.join('')); for (let i = 0, ii = rows.length; i < ii; i++) { if ((this.hasFrozenRows) && (rows[i] >= this.actualFrozenRow)) {