From cec789c563378d190529db4836b60d7e34878788 Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Tue, 18 Jun 2024 21:31:24 -0400 Subject: [PATCH] feat: add optional Top-Header for Drag Grouping + Header Grouping (#1029) --- cypress/e2e/example-draggable-grouping.cy.ts | 2 +- .../example-draggable-header-grouping.cy.ts | 218 +++++++ .../example-draggable-header-grouping.html | 544 ++++++++++++++++++ examples/index.html | 1 + src/models/gridOption.interface.ts | 16 +- src/plugins/slick.draggablegrouping.ts | 8 +- src/slick.grid.ts | 76 ++- src/styles/slick-alpine-theme.scss | 3 +- src/styles/slick-default-theme.scss | 3 +- 9 files changed, 858 insertions(+), 13 deletions(-) create mode 100644 cypress/e2e/example-draggable-header-grouping.cy.ts create mode 100644 examples/example-draggable-header-grouping.html diff --git a/cypress/e2e/example-draggable-grouping.cy.ts b/cypress/e2e/example-draggable-grouping.cy.ts index 1ed2f6a4..fdca9185 100644 --- a/cypress/e2e/example-draggable-grouping.cy.ts +++ b/cypress/e2e/example-draggable-grouping.cy.ts @@ -12,7 +12,7 @@ describe('Example - Draggable Grouping', { retries: 1 }, () => { cy.get('h2 + ul > li').first().contains('Draggable Grouping feature'); }); - it('should have exact column titles on both grids', () => { + it('should have exact column titles in grid', () => { cy.get('#myGrid') .find('.slick-header-columns') .children() diff --git a/cypress/e2e/example-draggable-header-grouping.cy.ts b/cypress/e2e/example-draggable-header-grouping.cy.ts new file mode 100644 index 00000000..5ec925a0 --- /dev/null +++ b/cypress/e2e/example-draggable-header-grouping.cy.ts @@ -0,0 +1,218 @@ +describe('Example - Draggable Grouping', { retries: 1 }, () => { + // 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 preHeaders = ['', 'Common Factor', 'Period', 'Analysis', '']; + const fullTitles = ['#', 'Title', 'Duration', 'Start', 'Finish', 'Cost', 'Effort-Driven']; + for (let i = 0; i < 30; i++) { + fullTitles.push(`Mock${i}`); + } + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/examples/example-draggable-header-grouping.html`); + cy.get('h2').contains('Demonstrates'); + cy.get('h2 + ul > li').first().contains('Draggable Grouping feature'); + }); + + it('should have exact column (pre-header) grouping titles in grid', () => { + cy.get('#myGrid') + .find('.slick-preheader-panel .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(preHeaders[index])); + }); + + it('should have exact column titles in grid', () => { + cy.get('#myGrid') + .find('.slick-header:not(.slick-preheader-panel) .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have 6x draggable icons', () => { + cy.get('.slick-column-groupable') + .should('have.length', 6); + + cy.get('.slick-column-groupable.sgi-drag-vertical') + .should('have.length', 6); + }); + + describe('Grouping Tests', () => { + it('should "Group by Duration & sort groups by value" then Collapse All and expect only group titles', () => { + cy.get('[data-test="add-50k-rows-btn"]').click(); + cy.get('[data-test="group-duration-sort-value-btn"]').click(); + cy.get('[data-test="collapse-all-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 4'); + }); + + it('should click on Expand All columns and expect 1st row as grouping title and 2nd row as a regular row', () => { + cy.get('[data-test="add-50k-rows-btn"]').click(); + cy.get('[data-test="group-duration-sort-value-btn"]').click(); + cy.get('[data-test="expand-all-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '0'); + }); + + it('should show 1 column title (Duration) shown in the pre-header section', () => { + cy.get('.slick-dropped-grouping:nth(0) div').contains('Duration'); + }); + + it('should "Group by Duration then Effort-Driven" and expect 1st row to be expanded, 2nd row to be expanded and 3rd row to be a regular row', () => { + cy.get('[data-test="group-duration-effort-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-title`).should('contain', 'Effort-Driven: False'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '0'); + }); + + it('should show 2 column titles (Duration, Effort-Driven) shown in the pre-header section', () => { + cy.get('.slick-dropped-grouping:nth(0) div').contains('Duration'); + cy.get('.slick-dropped-grouping:nth(1) div').contains('Effort-Driven'); + }); + + it('should be able to drag and swap pre-header grouped column titles (Effort-Driven, Duration)', () => { + cy.get('.slick-dropped-grouping:nth(0) div') + .contains('Duration') + .drag('.slick-dropped-grouping:nth(1) div'); + + cy.get('.slick-dropped-grouping:nth(0) div').contains('Effort-Driven'); + cy.get('.slick-dropped-grouping:nth(1) div').contains('Duration'); + }); + + it('should expect the grouping to be swapped as well in the grid', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '0'); + }); + + it('should use the topheader Toggle All button and expect all groups to now be collapsed', () => { + cy.get('.slick-topheader-panel .slick-group-toggle-all').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: True'); + }); + + it('should expand all rows with "Expand All" and expect all the Groups to be expanded and the Toogle All icon to be collapsed', () => { + cy.get('#myGrid') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('[data-test="expand-all-btn"]') + .click(); + + cy.get('#myGrid') + .find('.slick-group-toggle.collapsed') + .should('have.length', 0); + + cy.get('#myGrid') + .find('.slick-group-toggle.expanded') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + + cy.get('.slick-group-toggle-all.expanded') + .should('exist'); + }); + + it('should collapse all rows with "Collapse All" and expect all the Groups to be collapsed and the Toogle All icon to be collapsed', () => { + cy.get('#myGrid') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('[data-test="collapse-all-btn"]') + .click(); + + cy.get('#myGrid') + .find('.slick-group-toggle.expanded') + .should('have.length', 0); + + cy.get('#myGrid') + .find('.slick-group-toggle.collapsed') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + + cy.get('.slick-group-toggle-all.collapsed') + .should('exist'); + }); + + it('should use the topheader Toggle All button and expect all groups to now be expanded', () => { + cy.get('.slick-topheader-panel .slick-group-toggle-all').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`) + .should('have.css', 'marginLeft').and('eq', `0px`); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`) + .should('have.css', 'marginLeft').and('eq', `15px`); + }); + + it('should use the topheader Toggle All button again and expect all groups to now be collapsed', () => { + cy.get('.slick-topheader-panel .slick-group-toggle-all').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: True'); + }); + + it('should clear all groups with "Clear all Grouping" and expect all the Groups to be collapsed and the Toogle All icon to be collapsed', () => { + cy.get('#myGrid') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('[data-test="clear-grouping-btn"]') + .click(); + + cy.get('#myGrid') + .find('.slick-group-toggle-all') + .should('be.hidden'); + + cy.get('#myGrid') + .find('.slick-placeholder') + .should('be.visible') + .should('have.text', 'Drop a column header here to group by the column :)'); + }); + + it('should add 500 items and expect last row to be Task 500', () => { + cy.get('[data-test="add-500-rows-btn"]') + .click(); + + cy.get('#myGrid') + .find('.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom') + .wait(10); + + cy.get(`#myGrid [style="top: ${GRID_ROW_HEIGHT * 499}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 499'); + }); + + it('should add 50K items and expect last row to be Task 50,000', () => { + cy.get('[data-test="add-50k-rows-btn"]') + .click(); + + cy.get('#myGrid') + .find('.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom') + .wait(10); + + // cy.get(`#myGrid [style="top: ${GRID_ROW_HEIGHT * 49999}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 49999'); + cy.get(`#myGrid [style="top: 1.24998e+06px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 49999'); + }); + }); +}); diff --git a/examples/example-draggable-header-grouping.html b/examples/example-draggable-header-grouping.html new file mode 100644 index 00000000..a11879d0 --- /dev/null +++ b/examples/example-draggable-header-grouping.html @@ -0,0 +1,544 @@ + + + + + + + SlickGrid example: Droppable Grouping + + + + + + + + +
+
+
+ +
+
+
+ +
+

+ + Demonstrates: +

+
    +
  • Draggable Grouping feature
  • +
  • Similar to regular grouping but the "grouping" must be defined in the column we want to group by
  • +
+ Options: +
+
+ + +
+ +
+

+ + + +
+ + +
+ + + +
+ + + +
+ Load CSS Theme: + + +
+
+
+

Demonstrates:

+
    +
  • + Fully dynamic and interactive multi-level grouping with filtering and aggregates over 50'000 items
    + Each grouping level can have its own aggregates (over child rows, child groups, or all descendant rows).
    + Personally, this is just the coolest slickest thing I've ever seen done with DHTML grids! +
  • +
+

View Source:

+ +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/examples/index.html b/examples/index.html index 31958b4f..19a3d571 100644 --- a/examples/index.html +++ b/examples/index.html @@ -112,6 +112,7 @@

Grouping

  • Adding tree functionality (expand/collapse) to the grid
  • Adding tree functionality with tree sorting
  • Adding grouping using column dragging and dropping
  • +
  • Adding Header Grouping and Draggable Column Grouping & Dropping
  • Checkbox row selection with grouping and checkbox to select rows in group
  • Row Group with dedicated Grouping Column
  • diff --git a/src/models/gridOption.interface.ts b/src/models/gridOption.interface.ts index 4c174d2c..ee3b1962 100644 --- a/src/models/gridOption.interface.ts +++ b/src/models/gridOption.interface.ts @@ -87,12 +87,15 @@ export interface GridOption { /** Context menu options (mouse right+click) */ contextMenu?: ContextMenuOption; - /** Defaults to false, which leads to create the footer row of the grid */ + /** Defaults to false, which leads to creating the footer row of the grid */ createFooterRow?: boolean; - /** Default to false, which leads to create an extra pre-header panel (on top of column header) for column grouping purposes */ + /** Default to false, which leads to creating an extra pre-header panel (on top of column header) for column grouping purposes */ createPreHeaderPanel?: boolean; + /** Default to false, which leads to creating an extra top-header panel (on top of column header & pre-header) for column grouping purposes */ + createTopHeaderPanel?: boolean; + /** * Custom Tooltip Options, the tooltip could be defined in any of the Column Definition or in the Grid Options, * it will first try to find it in the Column that the user is hovering over or else (when not found) go and try to find it in the Grid Options @@ -254,6 +257,12 @@ export interface GridOption { /** Defaults to "auto", extra pre-header panel (on top of column header) width, it could be a number (pixels) or a string ("100%" or "auto") */ preHeaderPanelWidth?: number | string; + /** Extra top-header panel height (on top of column header & pre-header) */ + topHeaderPanelHeight?: number; + + /** Defaults to "auto", extra top-header panel (on top of column header & pre-header) width, it could be a number (pixels) or a string ("100%" or "auto") */ + topHeaderPanelWidth?: number | string; + /** Do we want to preserve copied selection on paste? */ preserveCopiedSelectionOnPaste?: boolean; @@ -297,6 +306,9 @@ export interface GridOption { /** Do we want to show the extra pre-header panel (on top of column header) for column grouping purposes */ showPreHeaderPanel?: boolean; + /** Do we want to show the extra top-header panel (on top of column header & pre-header) for column grouping purposes */ + showTopHeaderPanel?: boolean; + /** Do we want to show top panel row? */ showTopPanel?: boolean; diff --git a/src/plugins/slick.draggablegrouping.ts b/src/plugins/slick.draggablegrouping.ts index 071a4316..9fc2a3e3 100644 --- a/src/plugins/slick.draggablegrouping.ts +++ b/src/plugins/slick.draggablegrouping.ts @@ -19,7 +19,7 @@ const Utils = IIFE_ONLY ? Slick.Utils : Utils_; * github.com/muthukumarse/Slickgrid * * NOTES: - * This plugin provides the Draggable Grouping feature + * This plugin provides the Draggable Grouping feature which could be located in either the Top-Header or the Pre-Header * A plugin to add Draggable Grouping feature. * * USAGE: @@ -90,7 +90,7 @@ export class SlickDraggableGrouping { this._gridUid = this._grid.getUID(); this._gridColumns = this._grid.getColumns(); this._dataView = this._grid.getData(); - this._dropzoneElm = this._grid.getPreHeaderPanel(); + this._dropzoneElm = this._grid.getTopHeaderPanel() || this._grid.getPreHeaderPanel(); this._dropzoneElm.classList.add('slick-dropzone'); const dropPlaceHolderText = this._options.dropPlaceHolderText || 'Drop a column header here to group by the column'; @@ -151,7 +151,7 @@ export class SlickDraggableGrouping { */ getSetupColumnReorder(grid: SlickGrid, headers: any, _headerColumnWidthDiff: any, setColumns: (columns: Column[]) => void, setupColumnResize: () => void, _columns: Column[], getColumnIndex: (columnId: string) => number, _uid: string, trigger: (slickEvent: SlickEvent_, data?: any) => void) { this.destroySortableInstances(); - const dropzoneElm = grid.getPreHeaderPanel(); + const dropzoneElm = grid.getTopHeaderPanel() || grid.getPreHeaderPanel(); const groupTogglerElm = dropzoneElm.querySelector('.slick-group-toggle-all'); const sortableOptions = { @@ -260,7 +260,7 @@ export class SlickDraggableGrouping { this.onGroupChanged.unsubscribe(); this._handler.unsubscribeAll(); this._bindingEventService.unbindAll(); - Utils.emptyElement(document.querySelector(`.${this._gridUid} .slick-preheader-panel`)); + Utils.emptyElement(document.querySelector(`.${this._gridUid} .slick-preheader-panel,.${this._gridUid} .slick-topheader-panel`)); } protected destroySortableInstances() { diff --git a/src/slick.grid.ts b/src/slick.grid.ts index 0c18cfc4..8636a8a5 100644 --- a/src/slick.grid.ts +++ b/src/slick.grid.ts @@ -241,11 +241,15 @@ export class SlickGrid = Column, O e showFooterRow: false, footerRowHeight: 25, createPreHeaderPanel: false, + createTopHeaderPanel: false, showPreHeaderPanel: false, + showTopHeaderPanel: false, preHeaderPanelHeight: 25, showTopPanel: false, topPanelHeight: 25, preHeaderPanelWidth: 'auto', // mostly useful for Draggable Grouping dropzone to take full width + topHeaderPanelHeight: 25, + topHeaderPanelWidth: 'auto', // mostly useful for Draggable Grouping dropzone to take full width formatterFactory: null, editorFactory: null, cellFlashingCssClass: 'flashing', @@ -364,6 +368,9 @@ export class SlickGrid = Column, O e protected _preHeaderPanelR!: HTMLDivElement; protected _preHeaderPanelScrollerR!: HTMLDivElement; protected _preHeaderPanelSpacerR!: HTMLDivElement; + protected _topHeaderPanel!: HTMLDivElement; + protected _topHeaderPanelScroller!: HTMLDivElement; + protected _topHeaderPanelSpacer!: HTMLDivElement; protected _topPanelScrollers!: HTMLDivElement[]; protected _topPanels!: HTMLDivElement[]; protected _viewport!: HTMLDivElement[]; @@ -681,6 +688,17 @@ export class SlickGrid = Column, O e this._focusSink = Utils.createDomElement('div', { tabIndex: 0, style: { position: 'fixed', width: '0px', height: '0px', top: '0px', left: '0px', outline: '0px' } }, this._container); + if (this._options.createTopHeaderPanel) { + this._topHeaderPanelScroller = Utils.createDomElement('div', { className: 'slick-topheader-panel slick-state-default', style: { overflow: 'hidden', position: 'relative' } }, this._container); + this._topHeaderPanelScroller.appendChild(document.createElement('div')); + this._topHeaderPanel = Utils.createDomElement('div', null, this._topHeaderPanelScroller); + this._topHeaderPanelSpacer = Utils.createDomElement('div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._topHeaderPanelScroller); + + if (!this._options.showTopHeaderPanel) { + Utils.hide(this._topHeaderPanelScroller); + } + } + // Containers used for scrolling frozen columns and rows this._paneHeaderL = Utils.createDomElement('div', { className: 'slick-pane slick-pane-header slick-pane-left', tabIndex: 0 }, this._container); this._paneHeaderR = Utils.createDomElement('div', { className: 'slick-pane slick-pane-header slick-pane-right', tabIndex: 0 }, this._container); @@ -794,6 +812,11 @@ export class SlickGrid = Column, O e // Default the active canvas to the top left this._activeCanvasNode = this._canvasTopL; + // top-header + if (this._topHeaderPanelSpacer) { + Utils.width(this._topHeaderPanelSpacer, this.getCanvasWidth() + this.scrollbarDimensions.width); + } + // pre-header if (this._preHeaderPanelSpacer) { Utils.width(this._preHeaderPanelSpacer, this.getCanvasWidth() + this.scrollbarDimensions.width); @@ -915,6 +938,10 @@ export class SlickGrid = Column, O e }); } + if (this._options.createTopHeaderPanel) { + this._bindingEventService.bind(this._topHeaderPanelScroller, 'scroll', this.handleTopHeaderPanelScroll.bind(this) as EventListener); + } + if (this._options.createPreHeaderPanel) { this._bindingEventService.bind(this._preHeaderPanelScroller, 'scroll', this.handlePreHeaderPanelScroll.bind(this) as EventListener); } @@ -1191,6 +1218,10 @@ export class SlickGrid = Column, O e const oldCanvasWidthR = this.canvasWidthR; this.canvasWidth = this.getCanvasWidth(); + if (this._options.createTopHeaderPanel) { + Utils.width(this._topHeaderPanel, this._options.topHeaderPanelWidth ?? this.canvasWidth); + } + const widthChanged = this.canvasWidth !== oldCanvasWidth || this.canvasWidthL !== oldCanvasWidthL || this.canvasWidthR !== oldCanvasWidthR; if (widthChanged || this.hasFrozenColumns() || this.hasFrozenRows) { @@ -1458,6 +1489,11 @@ export class SlickGrid = Column, O e return this._preHeaderPanelR; } + /** Get the Top-Header Panel DOM node element */ + getTopHeaderPanel() { + return this._topHeaderPanel; + } + /** * Get Header Row Column DOM element by its column Id or index * @param {Number|String} columnIdOrIdx - column Id or index @@ -2378,6 +2414,7 @@ export class SlickGrid = Column, O e `.${this.uid} .slick-header-column { left: 1000px; }`, `.${this.uid} .slick-top-panel { height: ${this._options.topPanelHeight}px; }`, `.${this.uid} .slick-preheader-panel { height: ${this._options.preHeaderPanelHeight}px; }`, + `.${this.uid} .slick-topheader-panel { height: ${this._options.topHeaderPanelHeight}px; }`, `.${this.uid} .slick-headerrow-columns { height: ${this._options.headerRowHeight}px; }`, `.${this.uid} .slick-footerrow-columns { height: ${this._options.footerRowHeight}px; }`, `.${this.uid} .slick-cell { height: ${rowHeight}px; }`, @@ -2546,6 +2583,10 @@ export class SlickGrid = Column, O e this._bindingEventService.unbindByEventName(this._preHeaderPanelScroller, 'scroll'); } + if (this._topHeaderPanelScroller) { + this._bindingEventService.unbindByEventName(this._topHeaderPanelScroller, 'scroll'); + } + this._bindingEventService.unbindByEventName(this._focusSink, 'keydown'); this._bindingEventService.unbindByEventName(this._focusSink2, 'keydown'); @@ -3701,7 +3742,7 @@ export class SlickGrid = Column, O e return !Array.isArray(this.data); } - protected togglePanelVisibility(option: 'showTopPanel' | 'showHeaderRow' | 'showColumnHeader' | 'showFooterRow' | 'showPreHeaderPanel', container: HTMLElement | HTMLElement[], visible?: boolean, animate?: boolean) { + protected togglePanelVisibility(option: 'showTopPanel' | 'showHeaderRow' | 'showColumnHeader' | 'showFooterRow' | 'showPreHeaderPanel' | 'showTopHeaderPanel', container: HTMLElement | HTMLElement[], visible?: boolean, animate?: boolean) { const animated = (animate === false) ? false : true; if (this._options[option] !== visible) { @@ -3769,6 +3810,14 @@ export class SlickGrid = Column, O e this.togglePanelVisibility('showPreHeaderPanel', [this._preHeaderPanelScroller, this._preHeaderPanelScrollerR], visible, animate); } + /** + * Set the Top-Header Visibility + * @param {Boolean} [visible] - optionally set if top-header panel is visible or not + */ + setTopHeaderPanelVisibility(visible?: boolean) { + this.togglePanelVisibility('showTopHeaderPanel', this._topHeaderPanelScroller, visible); + } + /** Get Grid Canvas Node DOM Element */ getContainerNode() { return this._container; @@ -4266,6 +4315,7 @@ export class SlickGrid = Column, O e } else { const columnNamesH = (this._options.showColumnHeader) ? Utils.toFloat(Utils.height(this._headerScroller[0]) as number) + this.getVBoxDelta(this._headerScroller[0]) : 0; const preHeaderH = (this._options.createPreHeaderPanel && this._options.showPreHeaderPanel) ? this._options.preHeaderPanelHeight! + this.getVBoxDelta(this._preHeaderPanelScroller) : 0; + const topHeaderH = (this._options.createTopHeaderPanel && this._options.showTopHeaderPanel) ? this._options.topHeaderPanelHeight! + this.getVBoxDelta(this._topHeaderPanelScroller) : 0; const style = getComputedStyle(this._container); this.viewportH = Utils.toFloat(style.height) @@ -4275,7 +4325,8 @@ export class SlickGrid = Column, O e - this.topPanelH - this.headerRowH - this.footerRowH - - preHeaderH; + - preHeaderH + - topHeaderH; } this.numVisibleRows = Math.ceil(this.viewportH / this._options.rowHeight!); @@ -4330,7 +4381,13 @@ export class SlickGrid = Column, O e this._paneTopL.style.position = 'relative'; } - Utils.setStyleSize(this._paneTopL, 'top', Utils.height(this._paneHeaderL) || (this._options.showHeaderRow ? this._options.headerRowHeight! : 0) + (this._options.showPreHeaderPanel ? this._options.preHeaderPanelHeight! : 0)); + let topHeightOffset = Utils.height(this._paneHeaderL); + if (topHeightOffset) { + topHeightOffset += (this._options.showTopHeaderPanel ? this._options.topHeaderPanelHeight! : 0); + } else { + topHeightOffset = (this._options.showHeaderRow ? this._options.headerRowHeight! : 0) + (this._options.showPreHeaderPanel ? this._options.preHeaderPanelHeight! : 0); + } + Utils.setStyleSize(this._paneTopL, 'top', topHeightOffset || topHeightOffset); Utils.height(this._paneTopL, this.paneTopH); const paneBottomTop = this._paneTopL.offsetTop + this.paneTopH; @@ -4340,7 +4397,11 @@ export class SlickGrid = Column, O e } if (this.hasFrozenColumns()) { - Utils.setStyleSize(this._paneTopR, 'top', Utils.height(this._paneHeaderL) as number); + let topHeightOffset = Utils.height(this._paneHeaderL); + if (topHeightOffset) { + topHeightOffset += (this._options.showTopHeaderPanel ? this._options.topHeaderPanelHeight! : 0); + } + Utils.setStyleSize(this._paneTopR, 'top', topHeightOffset as number); Utils.height(this._paneTopR, this.paneTopH); Utils.height(this._viewportTopR, this.viewportTopH); @@ -4912,6 +4973,10 @@ export class SlickGrid = Column, O e this.handleElementScroll(this._preHeaderPanelScroller); } + protected handleTopHeaderPanelScroll() { + this.handleElementScroll(this._topHeaderPanelScroller); + } + protected handleElementScroll(element: HTMLElement) { const scrollLeft = element.scrollLeft; if (scrollLeft !== this._viewportScrollContainerX.scrollLeft) { @@ -4962,6 +5027,9 @@ export class SlickGrid = Column, O e this._preHeaderPanelScroller.scrollLeft = this.scrollLeft; } } + if (this._options.createTopHeaderPanel) { + this._topHeaderPanelScroller.scrollLeft = this.scrollLeft; + } if (this.hasFrozenColumns()) { if (this.hasFrozenRows) { diff --git a/src/styles/slick-alpine-theme.scss b/src/styles/slick-alpine-theme.scss index 27cb847a..0c87751b 100644 --- a/src/styles/slick-alpine-theme.scss +++ b/src/styles/slick-alpine-theme.scss @@ -259,7 +259,8 @@ filter: alpha(opacity = 70); } -.slick-preheader-panel { +.slick-preheader-panel, +.slick-topheader-panel { .slick-header-column { border-style: solid; border-color: var(--alpine-preheader-border-color, $alpine-preheader-border-color); diff --git a/src/styles/slick-default-theme.scss b/src/styles/slick-default-theme.scss index 27fecba5..1a0763f4 100644 --- a/src/styles/slick-default-theme.scss +++ b/src/styles/slick-default-theme.scss @@ -29,7 +29,8 @@ classes should alter those! height: 100%; } -.slick-preheader-panel { +.slick-preheader-panel, +.slick-topheader-panel { border: 1px solid #d3d3d3; }