From a35f0a488775e8ccb68ec8fe0ece9abc47c358f4 Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Sat, 9 Dec 2023 16:53:17 -0500 Subject: [PATCH] fix: regression, Row Detail no longer displayed after CSP safe code (#1259) --- packages/common/src/core/slickGrid.ts | 7 ++- .../formatterResultObject.interface.ts | 7 +++ packages/common/src/services/domUtilities.ts | 5 ++ .../src/slickRowDetailView.spec.ts | 8 ++-- .../src/slickRowDetailView.ts | 46 +++++++++---------- 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index e51356896..aa826f0fc 100644 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -77,7 +77,7 @@ import type { SlickPlugin, SlickGridEventData, } from '../interfaces'; -import { createDomElement, emptyElement, getOffset, getInnerSize } from '../services/domUtilities'; +import { createDomElement, emptyElement, getInnerSize, getOffset, insertAfterElement } from '../services/domUtilities'; /** * @license @@ -3481,6 +3481,11 @@ export class SlickGrid = Column, O e divRow.appendChild(cellDiv); + // Formatter can optional add an "insertElementAfterTarget" option but it must be inserted only after the `.slick-row` div exists + if ((formatterResult as FormatterResultObject).insertElementAfterTarget) { + insertAfterElement(cellDiv, (formatterResult as FormatterResultObject).insertElementAfterTarget as HTMLElement); + } + this.rowsCache[row].cellRenderQueue.push(cell); this.rowsCache[row].cellColSpans[cell] = colspan; } diff --git a/packages/common/src/interfaces/formatterResultObject.interface.ts b/packages/common/src/interfaces/formatterResultObject.interface.ts index 9284d5e52..580b96a7c 100644 --- a/packages/common/src/interfaces/formatterResultObject.interface.ts +++ b/packages/common/src/interfaces/formatterResultObject.interface.ts @@ -7,6 +7,13 @@ export interface FormatterResultObject { /** Optional tooltip text when hovering the cell div container. */ toolTip?: string; + + /** + * optionally insert an HTML element after the element target + * for example we use this technique to take a div containing the row detail and insert it after the `.slick-cell` + * e.g.:
...
+ */ + insertElementAfterTarget?: HTMLElement; } export interface FormatterResultWithText extends FormatterResultObject { diff --git a/packages/common/src/services/domUtilities.ts b/packages/common/src/services/domUtilities.ts index 534b1cc88..02fb4e514 100644 --- a/packages/common/src/services/domUtilities.ts +++ b/packages/common/src/services/domUtilities.ts @@ -348,6 +348,11 @@ export function htmlEncodeWithPadding(inputStr: string, paddingLength: number): return outputStr; } +/** insert an HTML Element after a target Element in the DOM */ +export function insertAfterElement(referenceNode: HTMLElement, newNode: HTMLElement) { + referenceNode.parentNode?.insertBefore(newNode, referenceNode.nextSibling); +} + /** * Sanitize possible dirty html string (remove any potential XSS code like scripts and others), we will use 2 possible sanitizer * 1. optional sanitizer method defined in the grid options diff --git a/packages/row-detail-view-plugin/src/slickRowDetailView.spec.ts b/packages/row-detail-view-plugin/src/slickRowDetailView.spec.ts index 2fcc2f50e..7d60ea34e 100644 --- a/packages/row-detail-view-plugin/src/slickRowDetailView.spec.ts +++ b/packages/row-detail-view-plugin/src/slickRowDetailView.spec.ts @@ -1,5 +1,5 @@ import 'jest-extended'; -import { Column, GridOption, PubSubService, type SlickDataView, SlickEvent, SlickEventData, SlickGrid } from '@slickgrid-universal/common'; +import { Column, GridOption, PubSubService, type SlickDataView, SlickEvent, SlickEventData, SlickGrid, FormatterResultWithHtml } from '@slickgrid-universal/common'; import { SlickRowDetailView } from './slickRowDetailView'; @@ -39,6 +39,7 @@ const gridStub = { invalidateRows: jest.fn(), registerPlugin: jest.fn(), render: jest.fn(), + sanitizeHtmlString: (s) => s, updateRowCount: jest.fn(), onBeforeEditCell: new SlickEvent(), onClick: new SlickEvent(), @@ -715,7 +716,7 @@ describe('SlickRowDetailView plugin', () => { plugin.setOptions({ collapsedClass: 'some-collapsed' }); plugin.expandableOverride(() => true); const formattedVal = plugin.getColumnDefinition().formatter!(0, 1, '', mockColumns[0], mockItem, gridStub); - expect(formattedVal).toBe(`
`); + expect((formattedVal as HTMLElement).outerHTML).toBe(`
`); }); it('should execute formatter and expect it to return empty string and render nothing when isPadding is True', () => { @@ -733,7 +734,8 @@ describe('SlickRowDetailView plugin', () => { plugin.setOptions({ expandedClass: 'some-expanded', maxRows: 2 }); plugin.expandableOverride(() => true); const formattedVal = plugin.getColumnDefinition().formatter!(0, 1, '', mockColumns[0], mockItem, gridStub); - expect(formattedVal).toBe(`
undefined
`); + expect(((formattedVal as FormatterResultWithHtml).html as HTMLElement).outerHTML).toBe(`
`); + expect((formattedVal as FormatterResultWithHtml).insertElementAfterTarget!.outerHTML).toBe(`
undefined
`); }); }); }); diff --git a/packages/row-detail-view-plugin/src/slickRowDetailView.ts b/packages/row-detail-view-plugin/src/slickRowDetailView.ts index f4791a44b..91afb2f91 100644 --- a/packages/row-detail-view-plugin/src/slickRowDetailView.ts +++ b/packages/row-detail-view-plugin/src/slickRowDetailView.ts @@ -3,7 +3,6 @@ import type { DOMMouseOrTouchEvent, ExternalResource, FormatterResultWithHtml, - FormatterResultWithText, GridOption, OnAfterRowDetailToggleArgs, OnBeforeRowDetailToggleArgs, @@ -16,11 +15,11 @@ import type { RowDetailViewOption, SlickGrid, SlickRowDetailView as UniversalRowDetailView, - UsabilityOverrideFn, SlickDataView, SlickEventData, + UsabilityOverrideFn, } from '@slickgrid-universal/common'; -import { SlickEvent, SlickEventHandler, } from '@slickgrid-universal/common'; +import { createDomElement, SlickEvent, SlickEventHandler, } from '@slickgrid-universal/common'; import { objectAssignAndExtend } from '@slickgrid-universal/utils'; /** @@ -606,7 +605,7 @@ export class SlickRowDetailView implements ExternalResource, UniversalRowDetailV } /** The Formatter of the toggling icon of the Row Detail */ - protected detailSelectionFormatter(row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: SlickGrid): FormatterResultWithHtml | FormatterResultWithText | string { + protected detailSelectionFormatter(row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: SlickGrid): FormatterResultWithHtml | HTMLElement | '' { if (!this.checkExpandableOverride(row, dataContext, grid)) { return ''; } else { @@ -626,9 +625,8 @@ export class SlickRowDetailView implements ExternalResource, UniversalRowDetailV if (this._addonOptions.collapsedClass) { collapsedClasses += this._addonOptions.collapsedClass; } - return `
`; + return createDomElement('div', { className: collapsedClasses.trim() }); } else { - const html: string[] = []; const rowHeight = this.gridOptions.rowHeight || 0; let outterHeight = (dataContext[`${this._keyPrefix}sizePadding`] || 0) * this.gridOptions.rowHeight!; @@ -637,28 +635,30 @@ export class SlickRowDetailView implements ExternalResource, UniversalRowDetailV dataContext[`${this._keyPrefix}sizePadding`] = this._addonOptions.maxRows; } - // V313HAX: - // putting in an extra closing div after the closing toggle div and ommiting a - // final closing div for the detail ctr div causes the slickgrid renderer to - // insert our detail div as a new column ;) ~since it wraps whatever we provide - // in a generic div column container. so our detail becomes a child directly of - // the row not the cell. nice =) ~no need to apply a css change to the parent - // slick-cell to escape the cell overflow clipping. - // sneaky extra
inserted here-----------------v let expandedClasses = `${this._addonOptions.cssClass || ''} collapse `; if (this._addonOptions.expandedClass) { expandedClasses += this._addonOptions.expandedClass; } - html.push(`
`); - html.push(`
`); // shift detail below 1st row - html.push(`
`); // sub ctr for custom styling - html.push(`
${dataContext[`${this._keyPrefix}detailContent`]}
`); - // omit a final closing detail container
that would come next - - return html.join(''); + + // create the Row Detail div container that will be inserted AFTER the `.slick-cell` + const cellDetailContainerElm = createDomElement('div', { + className: `dynamic-cell-detail cellDetailView_${dataContext[this._dataViewIdProperty]}`, + style: { height: `${outterHeight}px`, top: `${rowHeight}px` } + }); + const innerContainerElm = createDomElement('div', { className: `detail-container detailViewContainer_${dataContext[this._dataViewIdProperty]}` }); + const innerDetailViewElm = createDomElement('div', { className: `innerDetailView_${dataContext[this._dataViewIdProperty]}` }); + innerDetailViewElm.innerHTML = this._grid.sanitizeHtmlString(dataContext[`${this._keyPrefix}detailContent`]); + + innerContainerElm.appendChild(innerDetailViewElm); + cellDetailContainerElm.appendChild(innerContainerElm); + + const result: FormatterResultWithHtml = { + html: createDomElement('div', { className: expandedClasses }), + insertElementAfterTarget: cellDetailContainerElm, + }; + + return result; } } return '';