diff --git a/packages/common/src/core/__tests__/slickGrid.spec.ts b/packages/common/src/core/__tests__/slickGrid.spec.ts index a5ade25aa..a7ed24948 100644 --- a/packages/common/src/core/__tests__/slickGrid.spec.ts +++ b/packages/common/src/core/__tests__/slickGrid.spec.ts @@ -122,6 +122,29 @@ describe('SlickGrid core file', () => { expect(grid.getGridPosition()).toBeTruthy(); }); + it('should handle ancestor scrolling after the grid container is moved', () => { + const columns = [{ id: 'firstName', field: 'firstName', name: 'First Name' }] as Column[]; + const oldScrollContainer = document.createElement('div'); + const newScrollContainer = document.createElement('div'); + document.body.append(oldScrollContainer, newScrollContainer); + oldScrollContainer.appendChild(container); + + grid = new SlickGrid(container, [{ id: 0, firstName: 'Avery' }], columns, defaultOptions); + grid.init(); + grid.setActiveCell(0, 0); + const positionChangedSpy = vi.spyOn(grid.onActiveCellPositionChanged, 'notify'); + + oldScrollContainer.dispatchEvent(new Event('scroll', { bubbles: false })); + expect(positionChangedSpy).toHaveBeenCalledTimes(1); + + positionChangedSpy.mockClear(); + newScrollContainer.appendChild(container); + oldScrollContainer.dispatchEvent(new Event('scroll', { bubbles: false })); + newScrollContainer.dispatchEvent(new Event('scroll', { bubbles: false })); + + expect(positionChangedSpy).toHaveBeenCalledTimes(1); + }); + it('should be able to instantiate SlickGrid with an external PubSub Service', () => { const columns = [{ id: 'firstName', field: 'firstName', name: 'First Name' }] as Column[]; grid = new SlickGrid('#myGrid', [], columns, defaultOptions, pubSubServiceStub); @@ -7112,6 +7135,26 @@ describe('SlickGrid core file', () => { expect(secondItemAgeCell.classList.contains('highlight')).toBeFalsy(); }); + it('should merge keyed CSS style overlays before creating a cell', () => { + grid = new SlickGrid(container, items, columns, { ...defaultOptions, enableCellNavigation: true }); + grid.addCellCssStyles('primary', { 0: { age: 'primary-highlight' } }); + grid.addCellCssStyles('secondary', { 0: { age: 'secondary-highlight' } }); + + const initialRow = document.createElement('div'); + (grid as any).appendCellHtml(initialRow, 0, 1, 1, 1, null, items[0]); + expect(initialRow.firstElementChild?.classList.contains('primary-highlight')).toBe(true); + expect(initialRow.firstElementChild?.classList.contains('secondary-highlight')).toBe(true); + + grid.setCellCssStyles('primary', { 0: { age: 'replacement-highlight' } }); + grid.removeCellCssStyles('secondary'); + + const updatedRow = document.createElement('div'); + (grid as any).appendCellHtml(updatedRow, 0, 1, 1, 1, null, items[0]); + expect(updatedRow.firstElementChild?.classList.contains('primary-highlight')).toBe(false); + expect(updatedRow.firstElementChild?.classList.contains('secondary-highlight')).toBe(false); + expect(updatedRow.firstElementChild?.classList.contains('replacement-highlight')).toBe(true); + }); + it('should removeCellCssStyles of all matching keys by predicate', () => { grid = new SlickGrid(container, items, columns, { ...defaultOptions, enableCellNavigation: true }); const onCellStyleSpy = vi.spyOn(grid.onCellCssStylesChanged, 'notify'); diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index 0fd40d63a..f8c37413b 100755 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -392,7 +392,6 @@ export class SlickGrid = Column, O e protected _viewport!: HTMLDivElement[]; protected _canvas!: HTMLDivElement[]; protected _style?: HTMLStyleElement; - protected _boundAncestors: HTMLElement[] = []; protected stylesheet?: { cssRules: Array<{ selectorText: string }>; rules: Array<{ selectorText: string }> } | null; protected columnCssRulesL?: Array<{ selectorText: string }>; protected columnCssRulesR?: Array<{ selectorText: string }>; @@ -462,6 +461,7 @@ export class SlickGrid = Column, O e protected plugins: SlickPlugin[] = []; protected cellCssClasses: CssStyleHash = {}; + protected cellCssClassesByCell: CssStyleHash = {}; protected columnsById: Record = {}; protected visibleColumnsById: Record = {}; @@ -1574,16 +1574,18 @@ export class SlickGrid = Column, O e return this.absoluteColumnMinWidth; } - // TODO: this is static. we need to handle page mutation. protected bindAncestorScrollEvents(): void { - let elem: HTMLElement | null = this.hasFrozenRows && !this._options.frozenBottom ? this._canvasBottomL : this._canvasTopL; - while ((elem = elem!.parentNode as HTMLElement) !== document.body && elem) { - // bind to scroll containers only - if (elem === this._viewportTopL || elem.scrollWidth !== elem.clientWidth || elem.scrollHeight !== elem.clientHeight) { - this._boundAncestors.push(elem); - this._bindingEventService.bind(elem, 'scroll', this.handleActiveCellPositionChange.bind(this)); - } - } + this._bindingEventService.bind( + document, + 'scroll', + (event) => { + const target = event.target; + if (this._viewport.includes(target as HTMLDivElement) || (target instanceof Node && target.contains(this._container))) { + this.handleActiveCellPositionChange(); + } + }, + true + ); } /** @@ -3131,8 +3133,6 @@ export class SlickGrid = Column, O e this.sortableSideLeftInstance.destroy(); } - this._boundAncestors.length = 0; // reset array - this._focusSink?.remove(); this._focusSink2?.remove(); @@ -4414,12 +4414,10 @@ export class SlickGrid = Column, O e cellCss += ' active'; } - // TODO: merge them together in the setter - Object.keys(this.cellCssClasses).forEach((key) => { - if (this.cellCssClasses[key][row]?.[m.id]) { - cellCss += ` ${this.cellCssClasses[key][row][m.id]}`; - } - }); + const cellCssClasses = this.cellCssClassesByCell[row]?.[m.id]; + if (cellCssClasses) { + cellCss += ` ${cellCssClasses}`; + } let value: any = null; let formatterResult: FormatterResultWithHtml | FormatterResultWithText | HTMLElement | DocumentFragment | string = ''; @@ -6089,6 +6087,22 @@ export class SlickGrid = Column, O e } } + /** Merges the keyed CSS overlays once on update, rather than for every rendered cell. */ + protected updateCellCssClassesByCell(): void { + this.cellCssClassesByCell = {}; + + Object.values(this.cellCssClasses).forEach((hash) => { + Object.entries(hash).forEach(([row, cellClasses]) => { + const mergedRowClasses = (this.cellCssClassesByCell[row] ??= {}); + Object.entries(cellClasses).forEach(([columnId, cssClasses]) => { + if (cssClasses) { + mergedRowClasses[columnId] = mergedRowClasses[columnId] ? `${mergedRowClasses[columnId]} ${cssClasses}` : cssClasses; + } + }); + }); + }); + } + /** * Adds an "overlay" of CSS classes to cell DOM elements. SlickGrid can have many such overlays associated with different keys and they are frequently used by plugins. For example, SlickGrid uses this method internally to decorate selected cells with selectedCellCssClass (see options). * @param {String} key A unique key you can use in calls to setCellCssStyles and removeCellCssStyles. If a hash with that key has already been set, an exception will be thrown. @@ -6105,6 +6119,7 @@ export class SlickGrid = Column, O e } this.cellCssClasses[key] = hash; + this.updateCellCssClassesByCell(); this.updateCellCssStylesOnRenderedRows(hash, null); this.triggerEvent(this.onCellCssStylesChanged, { key, hash, grid: this }); } @@ -6117,6 +6132,7 @@ export class SlickGrid = Column, O e if (this.cellCssClasses[key]) { this.updateCellCssStylesOnRenderedRows(null, this.cellCssClasses[key]); delete this.cellCssClasses[key]; + this.updateCellCssClassesByCell(); this.triggerEvent(this.onCellCssStylesChanged, { key, hash: null, grid: this }); } } @@ -6143,6 +6159,7 @@ export class SlickGrid = Column, O e setCellCssStyles(key: string, hash: CssStyleHash): void { const prevHash = this.cellCssClasses[key]; this.cellCssClasses[key] = hash; + this.updateCellCssClassesByCell(); this.updateCellCssStylesOnRenderedRows(hash, prevHash); this.triggerEvent(this.onCellCssStylesChanged, { key, hash, grid: this }); }