From 9d9146230ed02f5ac0366f3ec31ba9db8b8f4cc4 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 24 Nov 2023 01:00:01 -0500 Subject: [PATCH 1/5] feat: convert GroupItemMetadataProvider Formatter to native HTML for CSP - in order to depend less on the use of innerHTML, we should convert internal code that have Formatter to use native HTML element to be more CSP compliant. However please note that the user will have to convert themselve the GroupTotals Formatter to native element as well for full CSP compliance on that section of the code --- packages/common/src/core/slickGrid.ts | 6 ++--- .../slickGroupItemMetadataProvider.spec.ts | 19 +++++++++----- .../slickGroupItemMetadataProvider.ts | 25 ++++++++++++++++--- .../src/interfaces/formatter.interface.ts | 2 +- .../__tests__/extension.service.spec.ts | 19 -------------- .../common/src/services/extension.service.ts | 8 ------ .../__tests__/slick-vanilla-grid.spec.ts | 2 ++ .../components/slick-vanilla-grid-bundle.ts | 3 +++ 8 files changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index 90f70d090..485bca9f3 100644 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -475,11 +475,11 @@ export class SlickGrid = Column, O e * @param target - target element to apply to * @param val - input value can be either a string or an HTMLElement */ - applyHtmlCode(target: HTMLElement, val: string | HTMLElement = '', sanitizerOptions?: DOMPurify_.Config) { + applyHtmlCode(target: HTMLElement, val: string | HTMLElement | DocumentFragment = '', sanitizerOptions?: DOMPurify_.Config) { if (target) { - if (val instanceof HTMLElement) { + if (val instanceof HTMLElement || val instanceof DocumentFragment) { target.appendChild(val); - } else { + } else if (typeof val === 'string') { let sanitizedText = val; if (typeof this._options?.sanitizer === 'function') { sanitizedText = this._options.sanitizer(val || ''); diff --git a/packages/common/src/extensions/__tests__/slickGroupItemMetadataProvider.spec.ts b/packages/common/src/extensions/__tests__/slickGroupItemMetadataProvider.spec.ts index f8835029d..812a91aa4 100644 --- a/packages/common/src/extensions/__tests__/slickGroupItemMetadataProvider.spec.ts +++ b/packages/common/src/extensions/__tests__/slickGroupItemMetadataProvider.spec.ts @@ -28,6 +28,7 @@ const dataViewStub = { } as unknown as SlickDataView; const gridStub = { + applyHtmlCode: (elm, val) => elm.innerHTML = val || '', autosizeColumns: jest.fn(), getActiveCell: jest.fn(), getColumnIndex: jest.fn(), @@ -115,21 +116,27 @@ describe('GroupItemMetadataProvider Service', () => { }); it('should return Grouping info formatted with a group level 0 without indentation when calling "defaultGroupCellFormatter" with option "enableExpandCollapse" set to True', () => { + service.init(gridStub); service.setOptions({ enableExpandCollapse: true }); - const output = service.getOptions().groupFormatter!(0, 0, 'test', mockColumns[0], { title: 'Some Title' }, gridStub); - expect(output).toBe('Some Title'); + const output = service.getOptions().groupFormatter!(0, 0, 'test', mockColumns[0], { title: 'Some Title' }, gridStub) as DocumentFragment; + const htmlContent = [].map.call(output.childNodes, x => x.outerHTML).join('') + expect(htmlContent).toBe('Some Title'); }); it('should return Grouping info formatted with a group level 2 with indentation of 30px when calling "defaultGroupCellFormatter" with option "enableExpandCollapse" set to True and level 2', () => { + service.init(gridStub); service.setOptions({ enableExpandCollapse: true, toggleCssClass: 'groupy-toggle', toggleExpandedCssClass: 'groupy-expanded' }); - const output = service.getOptions().groupFormatter!(0, 0, 'test', mockColumns[0], { level: 2, title: 'Some Title' }, gridStub); - expect(output).toBe('Some Title'); + const output = service.getOptions().groupFormatter!(0, 0, 'test', mockColumns[0], { level: 2, title: 'Some Title' }, gridStub) as DocumentFragment; + const htmlContent = [].map.call(output.childNodes, x => x.outerHTML).join('') + expect(htmlContent).toBe('Some Title'); }); it('should return Grouping info formatted with a group level 2 with indentation of 30px when calling "defaultGroupCellFormatter" with option "enableExpandCollapse" set to True and level 2', () => { + service.init(gridStub); service.setOptions({ enableExpandCollapse: true, toggleCssClass: 'groupy-toggle', toggleCollapsedCssClass: 'groupy-collapsed' }); - const output = service.getOptions().groupFormatter!(0, 0, 'test', mockColumns[0], { collapsed: true, level: 3, title: 'Some Title' }, gridStub); - expect(output).toBe('Some Title'); + const output = service.getOptions().groupFormatter!(0, 0, 'test', mockColumns[0], { collapsed: true, level: 3, title: 'Some Title' }, gridStub) as DocumentFragment; + const htmlContent = [].map.call(output.childNodes, x => x.outerHTML).join('') + expect(htmlContent).toBe('Some Title'); }); }); diff --git a/packages/common/src/extensions/slickGroupItemMetadataProvider.ts b/packages/common/src/extensions/slickGroupItemMetadataProvider.ts index d02e66fa2..7b8975891 100644 --- a/packages/common/src/extensions/slickGroupItemMetadataProvider.ts +++ b/packages/common/src/extensions/slickGroupItemMetadataProvider.ts @@ -7,6 +7,7 @@ import type { ItemMetadata, OnClickEventArgs, } from '../interfaces/index'; +import { createDomElement } from '../services/domUtilities'; /** * Provides item metadata for group (SlickGroup) and totals (SlickTotals) rows produced by the DataView. @@ -17,6 +18,7 @@ import type { * If "grid.registerPlugin(...)" is not called, expand & collapse will not work. */ export class SlickGroupItemMetadataProvider { + pluginName = 'SlickGroupItemMetadataProvider' as const; protected _eventHandler: SlickEventHandler; protected _grid!: SlickGrid; protected _options: GroupItemMetadataProviderOption; @@ -106,7 +108,7 @@ export class SlickGroupItemMetadataProvider { // protected functions // ------------------- - protected defaultGroupCellFormatter(_row: number, _cell: number, _value: any, _columnDef: Column, item: any): string { + protected defaultGroupCellFormatter(_row: number, _cell: number, _value: any, _columnDef: Column, item: any) { if (!this._options.enableExpandCollapse) { return item.title; } @@ -116,8 +118,25 @@ export class SlickGroupItemMetadataProvider { const marginLeft = `${groupLevel * indentation}px`; const toggleClass = item.collapsed ? this._options.toggleCollapsedCssClass : this._options.toggleExpandedCssClass; - return `` + - `${item.title || ''}`; + // use a DocumentFragment to avoid creating an extra div container + const containerElm = document.createDocumentFragment(); + + // 1. group toggle span + containerElm.appendChild(createDomElement('span', { + className: `${this._options.toggleCssClass} ${toggleClass}`, + ariaExpanded: String(!item.collapsed), + style: { marginLeft } + })); + + // 2. group title span + const groupTitleElm = createDomElement('span', { className: this._options.groupTitleCssClass || '' }); + groupTitleElm.setAttribute('level', groupLevel); + (item.title instanceof HTMLElement) + ? groupTitleElm.appendChild(item.title) + : this._grid.applyHtmlCode(groupTitleElm, item.title ?? ''); + containerElm.appendChild(groupTitleElm); + + return containerElm; } protected defaultTotalsCellFormatter(_row: number, _cell: number, _value: any, columnDef: Column, item: any, grid: SlickGrid) { diff --git a/packages/common/src/interfaces/formatter.interface.ts b/packages/common/src/interfaces/formatter.interface.ts index 7ab9f6ad2..da62c637f 100644 --- a/packages/common/src/interfaces/formatter.interface.ts +++ b/packages/common/src/interfaces/formatter.interface.ts @@ -1,4 +1,4 @@ import type { SlickGrid } from '../core/index'; import type { Column, FormatterResultWithHtml, FormatterResultWithText } from './index'; -export declare type Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: T, grid: SlickGrid) => string | HTMLElement | FormatterResultWithHtml | FormatterResultWithText; +export declare type Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: T, grid: SlickGrid) => string | HTMLElement | DocumentFragment | FormatterResultWithHtml | FormatterResultWithText; diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts index 94690816c..4fa3fe0df 100644 --- a/packages/common/src/services/__tests__/extension.service.spec.ts +++ b/packages/common/src/services/__tests__/extension.service.spec.ts @@ -19,7 +19,6 @@ import { SlickContextMenu, SlickDraggableGrouping, SlickGridMenu, - SlickGroupItemMetadataProvider, SlickHeaderButtons, SlickHeaderMenu, SlickRowMoveManager, @@ -354,8 +353,6 @@ describe('ExtensionService', () => { const output = service.getExtensionByName(ExtensionName.draggableGrouping); const pluginInstance = service.getExtensionInstanceByName(ExtensionName.draggableGrouping); - const groupMetaInstance = service.getExtensionInstanceByName(ExtensionName.groupItemMetaProvider); - const output2 = service.getExtensionByName(ExtensionName.groupItemMetaProvider); expect(onRegisteredMock).toHaveBeenCalledWith(expect.toBeObject()); expect(output!.instance instanceof SlickDraggableGrouping).toBeTrue(); @@ -363,7 +360,6 @@ describe('ExtensionService', () => { expect(pluginInstance).toBeTruthy(); expect(output!.instance).toEqual(pluginInstance); expect(output).toEqual({ name: ExtensionName.draggableGrouping, instance: pluginInstance } as ExtensionModel); - expect(output2).toEqual({ name: ExtensionName.groupItemMetaProvider, instance: groupMetaInstance } as ExtensionModel); }); it('should register the GridMenu addon when "enableGridMenu" is set in the grid options', () => { @@ -383,21 +379,6 @@ describe('ExtensionService', () => { expect(output!.instance instanceof SlickGridMenu).toBeTrue(); }); - it('should register the GroupItemMetaProvider addon when "enableGrouping" is set in the grid options', () => { - const gridOptionsMock = { enableGrouping: true } as GridOption; - const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - - service.bindDifferentExtensions(); - const output = service.getExtensionByName(ExtensionName.groupItemMetaProvider); - const pluginInstance = service.getExtensionInstanceByName(ExtensionName.groupItemMetaProvider); - - expect(gridSpy).toHaveBeenCalled(); - expect(output!.instance instanceof SlickGroupItemMetadataProvider).toBeTrue(); - expect(pluginInstance).toBeTruthy(); - expect(output!.instance).toEqual(pluginInstance); - expect(output).toEqual({ name: ExtensionName.groupItemMetaProvider, instance: pluginInstance } as ExtensionModel); - }); - it('should register the CheckboxSelector addon when "enableCheckboxSelector" is set in the grid options', () => { const columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }] as Column[]; const gridOptionsMock = { enableCheckboxSelector: true } as GridOption; diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts index 383319d68..f2acda5c1 100644 --- a/packages/common/src/services/extension.service.ts +++ b/packages/common/src/services/extension.service.ts @@ -246,14 +246,6 @@ export class ExtensionService { this._extensionList[ExtensionName.gridMenu] = { name: ExtensionName.gridMenu, instance: this._gridMenuControl }; } - // Grouping Plugin - // register the group item metadata provider to add expand/collapse group handlers - if (this.gridOptions.enableDraggableGrouping || this.gridOptions.enableGrouping) { - this._groupItemMetadataProviderService = this._groupItemMetadataProviderService ? this._groupItemMetadataProviderService : new SlickGroupItemMetadataProvider(); - this._groupItemMetadataProviderService.init(this.sharedService.slickGrid); - this._extensionList[ExtensionName.groupItemMetaProvider] = { name: ExtensionName.groupItemMetaProvider, instance: this._groupItemMetadataProviderService }; - } - // Header Button Plugin if (this.gridOptions.enableHeaderButton) { const headerButtonPlugin = new SlickHeaderButtons(this.extensionUtility, this.pubSubService, this.sharedService); diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts index 7a8e993bb..df7931b27 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts @@ -801,6 +801,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () expect(dataviewSpy).toHaveBeenCalledWith({ inlineFilters: false, groupItemMetadataProvider: expect.anything() }); expect(sharedService.groupItemMetadataProvider instanceof SlickGroupItemMetadataProvider).toBeTruthy(); expect(sharedMetaSpy).toHaveBeenCalledWith(expect.toBeObject()); + expect(mockGrid.registerPlugin).toHaveBeenCalled(); component.dispose(); }); @@ -815,6 +816,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () expect(dataviewSpy).toHaveBeenCalledWith({ inlineFilters: false, groupItemMetadataProvider: expect.anything() }); expect(sharedMetaSpy).toHaveBeenCalledWith(expect.toBeObject()); expect(sharedService.groupItemMetadataProvider instanceof SlickGroupItemMetadataProvider).toBeTruthy(); + expect(mockGrid.registerPlugin).toHaveBeenCalled(); component.dispose(); }); diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index 071423aab..906be29df 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -533,6 +533,9 @@ export class SlickVanillaGridBundle { this.sharedService.dataView = this.dataView as SlickDataView; this.sharedService.slickGrid = this.slickGrid as SlickGrid; this.sharedService.gridContainerElement = this._gridContainerElm; + if (this.groupItemMetadataProvider) { + this.slickGrid.registerPlugin(this.groupItemMetadataProvider); // register GroupItemMetadataProvider when Grouping is enabled + } this.extensionService.bindDifferentExtensions(); this.bindDifferentHooks(this.slickGrid, this._gridOptions, this.dataView as SlickDataView); From 13f40dcf1c5a272e647ffcae7cf764e7e7ed1e6d Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 24 Nov 2023 01:03:59 -0500 Subject: [PATCH 2/5] chore: add missing DocumentFragment in SlickGrid --- packages/common/src/core/slickGrid.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index 485bca9f3..5b5424b5a 100644 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -3364,7 +3364,7 @@ export class SlickGrid = Column, O e } let value: any = null; - let formatterResult: FormatterResultWithHtml | FormatterResultWithText | HTMLElement | string = ''; + let formatterResult: FormatterResultWithHtml | FormatterResultWithText | HTMLElement | DocumentFragment | string = ''; if (item) { value = this.getDataItemValueForColumn(item, m); formatterResult = this.getFormatter(row, m)(row, cell, value, m, item, this as unknown as SlickGrid); @@ -3539,7 +3539,7 @@ export class SlickGrid = Column, O e } /** Apply a Formatter Result to a Cell DOM Node */ - applyFormatResultToCellNode(formatterResult: FormatterResultWithHtml | FormatterResultWithText | string | HTMLElement, cellNode: HTMLDivElement, suppressRemove?: boolean) { + applyFormatResultToCellNode(formatterResult: FormatterResultWithHtml | FormatterResultWithText | string | HTMLElement | DocumentFragment, cellNode: HTMLDivElement, suppressRemove?: boolean) { if (formatterResult === null || formatterResult === undefined) { formatterResult = ''; } if (Object.prototype.toString.call(formatterResult) !== '[object Object]') { this.applyHtmlCode(cellNode, formatterResult as string | HTMLElement); From fec479d9b166b20b0718de0035b8d5259ef1dd30 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 24 Nov 2023 01:08:07 -0500 Subject: [PATCH 3/5] chore: add missing DocumentFragment in SlickGrid --- .../common/src/interfaces/excelCopyBufferOption.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts index 6654d8d7f..0d2a499a4 100644 --- a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts +++ b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts @@ -17,7 +17,7 @@ export interface ExcelCopyBufferOption { copiedCellStyleLayerKey?: string; /** option to specify a custom column value extractor function */ - dataItemColumnValueExtractor?: (item: any, columnDef: Column) => string | HTMLElement | FormatterResultWithHtml | FormatterResultWithText | null; + dataItemColumnValueExtractor?: (item: any, columnDef: Column) => string | HTMLElement | DocumentFragment | FormatterResultWithHtml | FormatterResultWithText | null; /** option to specify a custom column value setter function */ dataItemColumnValueSetter?: (item: any, columnDef: Column, value: any) => string | FormatterResultWithHtml | FormatterResultWithText | null; From 49b2fc86edd463d29f1ab3c9f8d5bc82a9f23a0c Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 24 Nov 2023 11:19:06 -0500 Subject: [PATCH 4/5] fix: `applyHtmlCode` util should always clear targeted native element --- packages/common/src/core/slickGrid.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index 5b5424b5a..9bb9784ca 100644 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -469,15 +469,18 @@ export class SlickGrid = Column, O e /** * Apply HTML code by 3 different ways depending on what is provided as input and what options are enabled. - * 1. value is an HTMLElement, then simply append the HTML to the target element. + * 1. value is an HTMLElement or DocumentFragment, then first empty the target and simply append the HTML to the target element. * 2. value is string and `enableHtmlRendering` is enabled, then use `target.innerHTML = value;` * 3. value is string and `enableHtmlRendering` is disabled, then use `target.textContent = value;` * @param target - target element to apply to * @param val - input value can be either a string or an HTMLElement + * @param options - `emptyTarget` will empty the target */ applyHtmlCode(target: HTMLElement, val: string | HTMLElement | DocumentFragment = '', sanitizerOptions?: DOMPurify_.Config) { if (target) { if (val instanceof HTMLElement || val instanceof DocumentFragment) { + // first empty target and then append new HTML element + emptyElement(target); target.appendChild(val); } else if (typeof val === 'string') { let sanitizedText = val; From fcf3a35c1227bf4a69d7a2b502893ed7ffe08568 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 24 Nov 2023 12:03:12 -0500 Subject: [PATCH 5/5] fix: `applyHtmlCode` should work with value type number --- packages/common/src/core/slickGrid.ts | 16 ++++++++++------ packages/common/src/global-grid-options.ts | 1 + .../src/slick-empty-warning.component.ts | 4 +--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index 9bb9784ca..ea06e8346 100644 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -474,20 +474,24 @@ export class SlickGrid = Column, O e * 3. value is string and `enableHtmlRendering` is disabled, then use `target.textContent = value;` * @param target - target element to apply to * @param val - input value can be either a string or an HTMLElement - * @param options - `emptyTarget` will empty the target + * @param options - `emptyTarget`, defaults to true, will empty the target. `sanitizerOptions` is to provide extra options when using `innerHTML` and the sanitizer */ - applyHtmlCode(target: HTMLElement, val: string | HTMLElement | DocumentFragment = '', sanitizerOptions?: DOMPurify_.Config) { + applyHtmlCode(target: HTMLElement, val: string | HTMLElement | DocumentFragment = '', options?: { emptyTarget?: boolean; sanitizerOptions?: any; }) { if (target) { if (val instanceof HTMLElement || val instanceof DocumentFragment) { // first empty target and then append new HTML element - emptyElement(target); + const emptyTarget = options?.emptyTarget !== false; + if (emptyTarget) { + emptyElement(target); + } target.appendChild(val); - } else if (typeof val === 'string') { + } else { let sanitizedText = val; if (typeof this._options?.sanitizer === 'function') { sanitizedText = this._options.sanitizer(val || ''); } else if (typeof DOMPurify?.sanitize === 'function') { - sanitizedText = DOMPurify.sanitize(val || '', sanitizerOptions || { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }); + const purifyOptions = (options?.sanitizerOptions ?? this._options.sanitizeHtmlOptions ?? { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }) as DOMPurify_.Config; + sanitizedText = DOMPurify.sanitize(val || '', purifyOptions); } if (this._options.enableHtmlRendering) { @@ -730,7 +734,7 @@ export class SlickGrid = Column, O e // disable text selection in grid cells except in input and textarea elements // (this is IE-specific, because selectstart event will only fire in IE) this._viewport.forEach((view) => { - this._bindingEventService.bind(view, 'selectstart', (event) => { + this._bindingEventService.bind(view, 'selectstart', (event: Event) => { if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) { return; } diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index 3c9f7fcba..758e1937f 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -260,6 +260,7 @@ export const GlobalGridOptions: Partial = { maxItemToInspectSingleColumnWidthByContent: 5000, widthToRemoveFromExceededWidthReadjustment: 50, }, + sanitizeHtmlOptions: { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }, // our default DOMPurify options treeDataOptions: { exportIndentMarginLeft: 5, exportIndentationLeadingChar: '͏͏͏͏͏͏͏͏͏·', diff --git a/packages/empty-warning-component/src/slick-empty-warning.component.ts b/packages/empty-warning-component/src/slick-empty-warning.component.ts index 72c9a64a2..8080c6bc1 100644 --- a/packages/empty-warning-component/src/slick-empty-warning.component.ts +++ b/packages/empty-warning-component/src/slick-empty-warning.component.ts @@ -94,12 +94,10 @@ export class SlickEmptyWarningComponent implements ExternalResource { } if (!this._warningLeftElement && gridCanvasLeftElm && gridCanvasRightElm) { - const sanitizedOptions = this.gridOptions?.sanitizeHtmlOptions ?? {}; - this._warningLeftElement = document.createElement('div'); this._warningLeftElement.classList.add(emptyDataClassName); this._warningLeftElement.classList.add('left'); - this.grid.applyHtmlCode(this._warningLeftElement, warningMessage, sanitizedOptions); + this.grid.applyHtmlCode(this._warningLeftElement, warningMessage); // clone the warning element and add the "right" class to it so we can distinguish this._warningRightElement = this._warningLeftElement.cloneNode(true) as HTMLDivElement;