import { BindingEventService } from '@slickgrid-universal/binding'; import { classNameToList, createDomElement, destroyAllElementProps, emptyElement, extend, getInnerSize, getOffset, insertAfterElement, isDefined, isDefinedNumber, isPrimitiveOrHTML, queueMicrotaskPolyfill, type CSSStyleDeclarationWritable, } from '@slickgrid-universal/utils'; import type { SortableEvent, Options as SortableOptions } from 'sortablejs'; import Sortable from 'sortablejs/modular/sortable.core.esm.js'; import type { TrustedHTML } from 'trusted-types/lib'; import type { SelectionModel } from '../enums/index.js'; import { copyCellToClipboard } from '../formatters/formatterUtilities.js'; import type { GridOption as BaseGridOption, CellPosition, CellSelectionMode, CellViewportRange, Column, ColumnMetadata, ColumnSort, CssStyleHash, CustomDataView, DOMEvent, DragPosition, DragRowMove, EditController, Editor, EditorArguments, EditorConstructor, ElementPosition, Formatter, FormatterResultObject, FormatterResultWithHtml, FormatterResultWithText, InteractionBase, ItemMetadata, MultiColumnSort, OnActivateChangedOptionsEventArgs, OnActiveCellChangedEventArgs, OnAddNewRowEventArgs, OnAfterSetColumnsEventArgs, OnAutosizeColumnsEventArgs, OnBeforeAppendCellEventArgs, OnBeforeCellEditorDestroyEventArgs, OnBeforeColumnsResizeEventArgs, OnBeforeEditCellEventArgs, OnBeforeFooterRowCellDestroyEventArgs, OnBeforeHeaderCellDestroyEventArgs, OnBeforeHeaderRowCellDestroyEventArgs, OnBeforeSetColumnsEventArgs, OnCellChangeEventArgs, OnCellCssStylesChangedEventArgs, OnClickEventArgs, OnColumnsDragEventArgs, OnColumnsEventArgs, OnColumnsReorderedEventArgs, OnColumnsResizeDblClickEventArgs, OnColumnsResizedEventArgs, OnCompositeEditorChangeEventArgs, OnContextMenuArgs, OnDblClickEventArgs, OnDragReplaceCellsEventArgs, OnFooterClickEventArgs, OnFooterContextMenuEventArgs, OnFooterRowCellRenderedEventArgs, OnHeaderCellRenderedEventArgs, OnHeaderClickEventArgs, OnHeaderContextMenuEventArgs, OnHeaderKeyDownEventArgs, OnHeaderMouseEventArgs, OnHeaderRowCellRenderedEventArgs, OnKeyDownEventArgs, OnPreHeaderClickEventArgs, OnPreHeaderContextMenuEventArgs, OnRenderedEventArgs, OnScrollEventArgs, OnSelectedRowsChangedEventArgs, OnSetOptionsEventArgs, OnValidationErrorEventArgs, PagingInfo, SingleColumnSort, SlickPlugin, } from '../interfaces/index.js'; import { preClickClassName, SlickDragExtendHandle, SlickEvent, SlickEventData, SlickGlobalEditorLock, SlickRange, SlickSelectionUtils, Utils, type BasePubSub, type SlickEditorLock, } from './slickCore.js'; import type { SlickDataView } from './slickDataview.js'; import { Draggable, MouseWheel, Resizable } from './slickInteractions.js'; import { applyHtmlToElement, runOptionalHtmlSanitizer } from './utils.js'; /** * @license * (c) 2009-present Michael Leibman * michael{dot}leibman{at}gmail{dot}com * http://github.com/mleibman/slickgrid * * Distributed under MIT license. * All rights reserved. * * SlickGrid v5.1.0 * * NOTES: * Cell/row DOM manipulations are done directly bypassing JS DOM manipulation methods. * This increases the speed dramatically, * but can only be done safely because there are no event handlers * or data associated with any cell/row DOM nodes. Cell editors must make sure they implement .destroy() * and do proper cleanup. */ // SlickGrid class implementation (available as SlickGrid) interface RowCaching { rowNode: HTMLElement[] | null; cellColSpans: Array; cellNodesByColumnIdx: HTMLElement[]; cellRenderQueue: any[]; } export class SlickGrid = Column, O extends BaseGridOption = BaseGridOption> { // -- Public API // Events onActiveCellChanged: SlickEvent; onActiveCellPositionChanged: SlickEvent<{ grid: SlickGrid }>; onActivateChangedOptions: SlickEvent; onAddNewRow: SlickEvent; onAfterSetColumns: SlickEvent; onAutosizeColumns: SlickEvent; onBeforeAppendCell: SlickEvent; onBeforeCellEditorDestroy: SlickEvent; onBeforeColumnsResize: SlickEvent; onBeforeDestroy: SlickEvent<{ grid: SlickGrid }>; onBeforeEditCell: SlickEvent; onBeforeFooterRowCellDestroy: SlickEvent; onBeforeHeaderCellDestroy: SlickEvent; onBeforeHeaderRowCellDestroy: SlickEvent; onBeforeRemoveCachedRow: SlickEvent<{ row: number; grid: SlickGrid }>; onBeforeSetColumns: SlickEvent; onBeforeSort: SlickEvent; onBeforeUpdateColumns: SlickEvent; onAfterUpdateColumns: SlickEvent; onCellChange: SlickEvent; onCellCssStylesChanged: SlickEvent; onClick: SlickEvent; onColumnsReordered: SlickEvent; onColumnsDrag: SlickEvent; onColumnsResized: SlickEvent; onColumnsResizeDblClick: SlickEvent; onCompositeEditorChange: SlickEvent; onContextMenu: SlickEvent; onDblClick: SlickEvent; onDrag: SlickEvent; onDragInit: SlickEvent; onDragStart: SlickEvent; onDragEnd: SlickEvent; onFooterClick: SlickEvent; onFooterContextMenu: SlickEvent; onFooterRowCellRendered: SlickEvent; onHeaderCellRendered: SlickEvent; onHeaderClick: SlickEvent; onHeaderContextMenu: SlickEvent; onHeaderMouseEnter: SlickEvent; onHeaderMouseLeave: SlickEvent; onHeaderMouseOver: SlickEvent; onHeaderMouseOut: SlickEvent; onHeaderKeyDown: SlickEvent; onHeaderRowCellRendered: SlickEvent; onHeaderRowMouseEnter: SlickEvent; onHeaderRowMouseLeave: SlickEvent; onHeaderRowMouseOver: SlickEvent; onHeaderRowMouseOut: SlickEvent; onKeyDown: SlickEvent; onMouseEnter: SlickEvent; onMouseLeave: SlickEvent; onPreHeaderClick: SlickEvent; onPreHeaderContextMenu: SlickEvent; onRendered: SlickEvent; onScroll: SlickEvent; onSelectedRowsChanged: SlickEvent; onSetOptions: SlickEvent; onSort: SlickEvent; onValidationError: SlickEvent; onViewportChanged: SlickEvent<{ grid: SlickGrid }>; onDragReplaceCells: SlickEvent; // --- // protected variables // shared across all grids on the page protected scrollbarDimensions?: { height: number; width: number }; protected maxSupportedCssHeight!: number; // browser's breaking point protected canvas: HTMLCanvasElement | null = null; protected canvas_context: CanvasRenderingContext2D | null = null; // settings protected _options!: O; protected _defaults: BaseGridOption = { invalidColumnFreezePickerCallback: (error) => alert(error), invalidColumnFreezeWidthCallback: (error) => alert(error), invalidColumnFreezeWidthMessage: '[SlickGrid] You are trying to freeze/pin more columns than the grid can support. ' + 'Make sure to have less columns pinned (on the left) than the actual visible grid width.', invalidColumnFreezePickerMessage: '[SlickGrid] Action not allowed and aborted, you need to have at least one or more column on the right section of the column freeze/pining. ' + 'You could alternatively "Unfreeze all the columns" before trying again.', skipFreezeColumnValidation: false, allowDragFromClosest: 'div.slick-cell.dnd, div.slick-cell.cell-reorder', alwaysShowVerticalScroll: false, alwaysAllowHorizontalScroll: false, explicitInitialization: false, rowHeight: 25, defaultColumnWidth: 80, enableHtmlRendering: true, enableAddRow: false, leaveSpaceForNewRows: false, editable: false, autoEdit: true, autoEditNewRow: true, autoCommitEdit: false, suppressActiveCellChangeOnEdit: false, enableCellNavigation: true, enableColumnReorder: true, unorderableColumnCssClass: 'unorderable', asyncEditorLoading: false, asyncEditorLoadDelay: 100, forceFitColumns: false, enableAsyncPostRender: false, asyncPostRenderDelay: 50, enableAsyncPostRenderCleanup: false, asyncPostRenderCleanupDelay: 40, columnResizingDelay: 300, nonce: '', editorLock: SlickGlobalEditorLock, showColumnHeader: true, showHeaderRow: false, headerRowHeight: 25, createFooterRow: false, showFooterRow: false, footerRowHeight: 25, createPreHeaderPanel: false, createTopHeaderPanel: false, showPreHeaderPanel: false, showTopHeaderPanel: false, preHeaderPanelHeight: 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 showTopPanel: false, topPanelHeight: 25, formatterFactory: null, editorFactory: null, cellFlashingCssClass: 'flashing', rowHighlightCssClass: 'highlight-animate', rowHighlightDuration: 400, selectedCellCssClass: 'selected', multiSelect: true, enableCellRowSpan: false, enableTextSelectionOnCells: false, dataItemColumnValueExtractor: null, frozenBottom: false, frozenColumn: -1, frozenRow: -1, frozenRightViewportMinWidth: 100, fullWidthRows: false, multiColumnSort: false, numberedMultiColumnSort: false, tristateMultiColumnSort: false, sortColNumberInSeparateSpan: false, defaultFormatter: this.defaultFormatter, forceSyncScrolling: false, addNewRowCssClass: 'new-row', preserveCopiedSelectionOnPaste: false, preventDragFromKeys: ['ctrlKey', 'metaKey'], showCellSelection: true, viewportClass: undefined, minRowBuffer: 3, emulatePagingWhenScrolling: true, // when scrolling off bottom of viewport, place new row at top of viewport editorCellNavOnLRKeys: false, enableMouseWheelScrollHandler: true, doPaging: true, rowTopOffsetRenderType: 'top', scrollRenderThrottling: 10, suppressCssChangesOnHiddenInit: false, ffMaxSupportedCssHeight: 6000000, maxSupportedCssHeight: 1000000000, maxPartialRowSpanRemap: 5000, sanitizer: undefined, // sanitize function mixinDefaults: false, shadowRoot: undefined, }; protected _columnDefaults = { name: '', headerCssClass: null, defaultSortAsc: true, focusable: true, hidden: false, minWidth: 30, maxWidth: undefined, rerenderOnResize: false, reorderable: true, resizable: true, sortable: false, selectable: true, } as Partial; protected _columnResizeTimer?: any; protected _executionBlockTimer?: any; protected _flashCellTimer?: any; protected _highlightRowTimer?: any; // scroller protected th!: number; // virtual height protected h!: number; // real scrollable height protected ph!: number; // page height protected n!: number; // number of pages protected cj!: number; // "jumpiness" coefficient protected page = 0; // current page protected offset = 0; // current page offset protected vScrollDir = 1; protected _bindingEventService: BindingEventService = new BindingEventService(); protected initialized = false; protected _container!: HTMLElement; protected uid = `slickgrid_${Math.round(1000000 * Math.random())}`; protected dragReplaceEl: SlickDragExtendHandle = new SlickDragExtendHandle(this.uid); protected _focusSink!: HTMLDivElement; protected _focusSink2!: HTMLDivElement; protected _groupHeaders: HTMLDivElement[] = []; protected _headerScroller: HTMLDivElement[] = []; protected _headers: HTMLDivElement[] = []; protected _headerRows!: HTMLDivElement[]; protected _headerRowScroller!: HTMLDivElement[]; protected _headerRowSpacerL!: HTMLDivElement; protected _headerRowSpacerR!: HTMLDivElement; protected _footerRow!: HTMLDivElement[]; protected _footerRowScroller!: HTMLDivElement[]; protected _footerRowSpacerL!: HTMLDivElement; protected _footerRowSpacerR!: HTMLDivElement; protected _preHeaderPanel!: HTMLDivElement; protected _preHeaderPanelScroller!: HTMLDivElement; protected _preHeaderPanelSpacer!: HTMLDivElement; 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[]; 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 }>; protected viewportH = 0; protected viewportW = 0; protected canvasWidth = 0; protected canvasWidthL = 0; protected canvasWidthR = 0; protected headersWidth = 0; protected headersWidthL = 0; protected headersWidthR = 0; protected viewportHasHScroll = false; protected viewportHasVScroll = false; protected headerColumnWidthDiff = 0; protected headerColumnHeightDiff = 0; // border+padding protected cellWidthDiff = 0; protected cellHeightDiff = 0; protected absoluteColumnMinWidth!: number; protected hasFrozenRows = false; protected frozenRowsHeight = 0; protected actualFrozenRow = -1; protected _prevFrozenColumnIdx = -1; /** flag to indicate if invalid frozen alert has been shown already or not? This is to avoid showing it more than once */ protected _invalidfrozenAlerted = false; protected paneTopH = 0; protected paneBottomH = 0; protected viewportTopH = 0; protected viewportBottomH = 0; protected topPanelH = 0; protected headerRowH = 0; protected footerRowH = 0; protected tabbingDirection = 1; protected _activeCanvasNode!: HTMLDivElement; protected _activeViewportNode!: HTMLDivElement; protected activePosX!: number; protected activePosY!: number; protected activeRow!: number; protected activeCell!: number; protected activeCellNode: HTMLDivElement | null = null; protected currentEditor: Editor | null = null; protected serializedEditorValue: any; protected editController?: EditController; protected _prevDataLength = 0; protected _prevInvalidatedRowsCount = 0; protected _rowSpanIsCached = false; protected _colsWithRowSpanCache: { [colIdx: number]: Set } = {}; protected rowsCache: Record = {}; protected renderedRows = 0; protected numVisibleRows = 0; protected prevScrollTop = 0; protected scrollHeight = 0; protected scrollTop = 0; protected lastRenderedScrollTop = 0; protected lastRenderedScrollLeft = 0; protected prevScrollLeft = 0; protected scrollLeft = 0; protected selectionBottomRow!: number; protected selectionRightCell!: number; protected selectionModel?: SelectionModel; protected selectedRows: number[] = []; protected selectedRanges: SlickRange[] = []; protected plugins: SlickPlugin[] = []; protected cellCssClasses: CssStyleHash = {}; protected columnsById: Record = {}; protected visibleColumnsById: Record = {}; protected sortColumns: ColumnSort[] = []; protected columnPosLeft: number[] = []; protected columnPosRight: number[] = []; protected pagingActive = false; protected pagingIsLastPage = false; protected scrollThrottle!: { enqueue: () => void; dequeue: () => void }; // async call handles protected h_editorLoader?: any; protected h_postrender?: any; protected h_postrenderCleanup?: any; protected postProcessedRows: any = {}; protected postProcessToRow: number = null as any; protected postProcessFromRow: number = null as any; protected postProcessedCleanupQueue: Array<{ actionType: string; groupId: number; node: HTMLElement | HTMLElement[]; columnIdx?: number; rowIdx?: number; }> = []; protected postProcessgroupId = 0; // perf counters protected counter_rows_rendered = 0; protected counter_rows_removed = 0; protected _paneHeaderL!: HTMLDivElement; protected _paneHeaderR!: HTMLDivElement; protected _paneTopL!: HTMLDivElement; protected _paneTopR!: HTMLDivElement; protected _paneBottomL!: HTMLDivElement; protected _paneBottomR!: HTMLDivElement; protected _headerScrollerL!: HTMLDivElement; protected _headerScrollerR!: HTMLDivElement; protected _headerL!: HTMLDivElement; protected _headerR!: HTMLDivElement; protected _groupHeadersL!: HTMLDivElement; protected _groupHeadersR!: HTMLDivElement; protected _headerRowScrollerL!: HTMLDivElement; protected _headerRowScrollerR!: HTMLDivElement; protected _footerRowScrollerL!: HTMLDivElement; protected _footerRowScrollerR!: HTMLDivElement; protected _headerRowL!: HTMLDivElement; protected _headerRowR!: HTMLDivElement; protected _footerRowL!: HTMLDivElement; protected _footerRowR!: HTMLDivElement; protected _topPanelScrollerL!: HTMLDivElement; protected _topPanelScrollerR!: HTMLDivElement; protected _topPanelL!: HTMLDivElement; protected _topPanelR!: HTMLDivElement; protected _viewportTopL!: HTMLDivElement; protected _viewportTopR!: HTMLDivElement; protected _viewportBottomL!: HTMLDivElement; protected _viewportBottomR!: HTMLDivElement; protected _canvasTopL!: HTMLDivElement; protected _canvasTopR!: HTMLDivElement; protected _canvasBottomL!: HTMLDivElement; protected _canvasBottomR!: HTMLDivElement; protected _viewportScrollContainerX!: HTMLDivElement; protected _viewportScrollContainerY!: HTMLDivElement; protected _headerScrollContainer!: HTMLDivElement; protected _headerRowScrollContainer!: HTMLDivElement; protected _footerRowScrollContainer!: HTMLDivElement; // store css attributes if display:none is active in container or parent protected cssShow = { position: 'absolute', visibility: 'hidden', display: 'block' }; protected _hiddenParents: HTMLElement[] = []; protected oldProps: Array> = []; protected enforceFrozenRowHeightRecalc = false; protected columnResizeDragging = false; protected slickDraggableInstance: InteractionBase | null = null; protected slickMouseWheelInstances: Array = []; protected slickResizableInstances: Array = []; protected sortableSideLeftInstance?: ReturnType; protected sortableSideRightInstance?: ReturnType; protected _pubSubService?: BasePubSub; /** * Creates a new instance of the grid. * @class SlickGrid * @constructor * @param {Node} container - Container node to create the grid in. * @param {Array|Object} data - An array of objects for databinding or an external DataView. * @param {Array} columns - An array of column definitions. * @param {Object} [options] - Grid Options * @param {Object} [externalPubSub] - optional External PubSub Service to use by SlickEvent **/ constructor( protected readonly container: HTMLElement | string, protected data: CustomDataView | TData[], protected columns: C[], options: Partial, protected readonly externalPubSub?: BasePubSub | undefined ) { this._container = typeof this.container === 'string' ? (document.querySelector(this.container) as HTMLDivElement) : this.container; if (!this._container) { throw new Error(`SlickGrid requires a valid container, ${this.container} does not exist in the DOM.`); } this._pubSubService = externalPubSub; this.onActiveCellChanged = new SlickEvent('onActiveCellChanged', externalPubSub); this.onActiveCellPositionChanged = new SlickEvent<{ grid: SlickGrid }>('onActiveCellPositionChanged', externalPubSub); this.onAddNewRow = new SlickEvent('onAddNewRow', externalPubSub); this.onAfterSetColumns = new SlickEvent('onAfterSetColumns', externalPubSub); this.onAutosizeColumns = new SlickEvent('onAutosizeColumns', externalPubSub); this.onBeforeAppendCell = new SlickEvent('onBeforeAppendCell', externalPubSub); this.onBeforeCellEditorDestroy = new SlickEvent('onBeforeCellEditorDestroy', externalPubSub); this.onBeforeColumnsResize = new SlickEvent('onBeforeColumnsResize', externalPubSub); this.onBeforeDestroy = new SlickEvent<{ grid: SlickGrid }>('onBeforeDestroy', externalPubSub); this.onBeforeEditCell = new SlickEvent('onBeforeEditCell', externalPubSub); // prettier-ignore this.onBeforeFooterRowCellDestroy = new SlickEvent('onBeforeFooterRowCellDestroy', externalPubSub); this.onBeforeHeaderCellDestroy = new SlickEvent('onBeforeHeaderCellDestroy', externalPubSub); // prettier-ignore this.onBeforeHeaderRowCellDestroy = new SlickEvent('onBeforeHeaderRowCellDestroy', externalPubSub); this.onBeforeRemoveCachedRow = new SlickEvent<{ row: number; grid: SlickGrid }>('onRowRemovedFromCache', externalPubSub); this.onBeforeSetColumns = new SlickEvent('onBeforeSetColumns', externalPubSub); this.onBeforeSort = new SlickEvent('onBeforeSort', externalPubSub); this.onBeforeUpdateColumns = new SlickEvent('onBeforeUpdateColumns', externalPubSub); this.onAfterUpdateColumns = new SlickEvent('onBeforeUpdateColumns', externalPubSub); this.onCellChange = new SlickEvent('onCellChange', externalPubSub); this.onCellCssStylesChanged = new SlickEvent('onCellCssStylesChanged', externalPubSub); this.onClick = new SlickEvent('onClick', externalPubSub); this.onColumnsReordered = new SlickEvent('onColumnsReordered', externalPubSub); this.onColumnsDrag = new SlickEvent('onColumnsDrag', externalPubSub); this.onColumnsResized = new SlickEvent('onColumnsResized', externalPubSub); this.onColumnsResizeDblClick = new SlickEvent('onColumnsResizeDblClick', externalPubSub); this.onCompositeEditorChange = new SlickEvent('onCompositeEditorChange', externalPubSub); this.onContextMenu = new SlickEvent('onContextMenu', externalPubSub); this.onDblClick = new SlickEvent('onDblClick', externalPubSub); this.onDrag = new SlickEvent('onDrag', externalPubSub); this.onDragInit = new SlickEvent('onDragInit', externalPubSub); this.onDragStart = new SlickEvent('onDragStart', externalPubSub); this.onDragEnd = new SlickEvent('onDragEnd', externalPubSub); this.onFooterClick = new SlickEvent('onFooterClick', externalPubSub); this.onFooterContextMenu = new SlickEvent('onFooterContextMenu', externalPubSub); this.onFooterRowCellRendered = new SlickEvent('onFooterRowCellRendered', externalPubSub); this.onHeaderCellRendered = new SlickEvent('onHeaderCellRendered', externalPubSub); this.onHeaderClick = new SlickEvent('onHeaderClick', externalPubSub); this.onHeaderContextMenu = new SlickEvent('onHeaderContextMenu', externalPubSub); this.onHeaderMouseEnter = new SlickEvent('onHeaderMouseEnter', externalPubSub); this.onHeaderMouseLeave = new SlickEvent('onHeaderMouseLeave', externalPubSub); this.onHeaderMouseOver = new SlickEvent('onHeaderMouseOver', externalPubSub); this.onHeaderMouseOut = new SlickEvent('onHeaderMouseOut', externalPubSub); this.onHeaderRowMouseOver = new SlickEvent('onHeaderRowMouseOver', externalPubSub); this.onHeaderRowMouseOut = new SlickEvent('onHeaderRowMouseOut', externalPubSub); this.onHeaderKeyDown = new SlickEvent('onHeaderKeyDown', externalPubSub); this.onHeaderRowCellRendered = new SlickEvent('onHeaderRowCellRendered', externalPubSub); this.onHeaderRowMouseEnter = new SlickEvent('onHeaderRowMouseEnter', externalPubSub); this.onHeaderRowMouseLeave = new SlickEvent('onHeaderRowMouseLeave', externalPubSub); this.onKeyDown = new SlickEvent('onKeyDown', externalPubSub); this.onMouseEnter = new SlickEvent('onMouseEnter', externalPubSub); this.onMouseLeave = new SlickEvent('onMouseLeave', externalPubSub); this.onPreHeaderClick = new SlickEvent('onPreHeaderClick', externalPubSub); this.onPreHeaderContextMenu = new SlickEvent('onPreHeaderContextMenu', externalPubSub); this.onRendered = new SlickEvent('onRendered', externalPubSub); this.onScroll = new SlickEvent('onScroll', externalPubSub); this.onSelectedRowsChanged = new SlickEvent('onSelectedRowsChanged', externalPubSub); this.onSetOptions = new SlickEvent('onSetOptions', externalPubSub); this.onActivateChangedOptions = new SlickEvent('onActivateChangedOptions', externalPubSub); this.onSort = new SlickEvent('onSort', externalPubSub); this.onValidationError = new SlickEvent('onValidationError', externalPubSub); this.onViewportChanged = new SlickEvent<{ grid: SlickGrid }>('onViewportChanged', externalPubSub); this.onDragReplaceCells = new SlickEvent('onDragReplaceCells', externalPubSub); this.initialize(options); } // Initialization /** Initializes the grid. */ init(): void { if (!this._options.silenceWarnings && document.body.style.zoom && document.body.style.zoom !== '100%') { console.warn( '[Slickgrid] Zoom level other than 100% is not supported by the library and will give subpar experience. ' + 'SlickGrid relies on the `rowHeight` grid option to do row positioning & calculation and when zoom is not 100% then calculation becomes all offset.' ); } if (this._options.rowTopOffsetRenderType === 'transform' && (this._options.enableCellRowSpan || this._options.enableRowDetailView)) { console.warn( '[Slickgrid-Universal] `rowTopOffsetRenderType` should be set to "top" when using either RowDetail and/or RowSpan since "transform" is known to have UI issues.' ); } this.finishInitialization(); } protected initialize(options: Partial): void { // calculate these only once and share between grid instances if (options?.mixinDefaults) { // use provided options and then assign defaults if (!this._options) { this._options = options as O; } Utils.applyDefaults(this._options, this._defaults); } else { this._options = extend(true, {}, this._defaults, options); } this.scrollThrottle = this.actionThrottle(this.render.bind(this), this._options.scrollRenderThrottling as number); this.maxSupportedCssHeight = this.maxSupportedCssHeight || this.getMaxSupportedCssHeight(); this.validateAndEnforceOptions(); this._columnDefaults.width = this._options.defaultColumnWidth; this._prevFrozenColumnIdx = this.getFrozenColumnIdx(); if (!this._options.suppressCssChangesOnHiddenInit) { this.cacheCssForHiddenInit(); } this.updateColumnProps(); this.editController = { commitCurrentEdit: this.commitCurrentEdit.bind(this), cancelCurrentEdit: this.cancelCurrentEdit.bind(this), }; emptyElement(this._container); this._container.style.outline = String(0); this._container.classList.add(this.uid); this._container.classList.add('slick-widget'); this._container.setAttribute('role', 'grid'); this._container.setAttribute('aria-colcount', this.columns.length.toString()); this._container.setAttribute('aria-rowcount', Array.isArray(this.data) ? this.data.length.toString() : '0'); const containerStyles = getComputedStyle(this._container); if (!/relative|absolute|fixed/.test(containerStyles.position)) { this._container.style.position = 'relative'; } this._focusSink = createDomElement( 'div', { tabIndex: -1, style: { position: 'fixed', width: '0px', height: '0px', top: '0px', left: '0px', outline: '0px' } }, this._container ); if (this._options.createTopHeaderPanel) { this._topHeaderPanelScroller = createDomElement( 'div', { className: 'slick-topheader-panel slick-state-default', style: { overflow: 'hidden', position: 'relative' } }, this._container ); this._topHeaderPanelScroller.appendChild(document.createElement('div')); this._topHeaderPanel = createDomElement('div', null, this._topHeaderPanelScroller); this._topHeaderPanelSpacer = 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 = createDomElement('div', { className: 'slick-pane slick-pane-header slick-pane-left' }, this._container); this._paneHeaderR = createDomElement('div', { className: 'slick-pane slick-pane-header slick-pane-right' }, this._container); this._paneTopL = createDomElement('div', { className: 'slick-pane slick-pane-top slick-pane-left' }, this._container); this._paneTopR = createDomElement('div', { className: 'slick-pane slick-pane-top slick-pane-right' }, this._container); this._paneBottomL = createDomElement('div', { className: 'slick-pane slick-pane-bottom slick-pane-left' }, this._container); this._paneBottomR = createDomElement('div', { className: 'slick-pane slick-pane-bottom slick-pane-right' }, this._container); if (this._options.createPreHeaderPanel) { const headerContainer = createDomElement('div', { className: 'slick-preheader-container' }, this._paneHeaderL); this._preHeaderPanelScroller = createDomElement( 'div', { className: 'slick-preheader-panel slick-state-default', style: { overflow: 'hidden', position: 'relative' } }, headerContainer ); this._preHeaderPanelScroller.appendChild(document.createElement('div')); this._preHeaderPanel = createDomElement('div', null, this._preHeaderPanelScroller); this._preHeaderPanelSpacer = createDomElement( 'div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._preHeaderPanelScroller ); this._preHeaderPanelScrollerR = createDomElement( 'div', { className: 'slick-preheader-panel slick-state-default', style: { overflow: 'hidden', position: 'relative' } }, this._paneHeaderR ); this._preHeaderPanelR = createDomElement('div', null, this._preHeaderPanelScrollerR); this._preHeaderPanelSpacerR = createDomElement( 'div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._preHeaderPanelScrollerR ); if (!this._options.showPreHeaderPanel) { Utils.hide(this._preHeaderPanelScroller); Utils.hide(this._preHeaderPanelScrollerR); } } // Append the header scroller containers const headerContainerL = createDomElement('div', { className: 'slick-header-container' }, this._paneHeaderL); const headerContainerR = createDomElement('div', { className: 'slick-header-container' }, this._paneHeaderR); this._headerScrollerL = createDomElement('div', { className: 'slick-header slick-state-default slick-header-left' }, headerContainerL); this._headerScrollerR = createDomElement('div', { className: 'slick-header slick-state-default slick-header-right' }, headerContainerR); // header scroll position could change when using frozen grid and tabbing on next available header // so we need to make sure that all containers (header, headerrow, toppanel) are all in sync when that happens this._bindingEventService.bind(this._headerScrollerR, 'scroll', (e) => { this.scrollToX((e.target as HTMLElement).scrollLeft); }); // Cache the header scroller containers this._headerScroller.push(this._headerScrollerL); this._headerScroller.push(this._headerScrollerR); // Append the columnn containers to the headers this._headerL = createDomElement( 'div', { className: 'slick-header-columns slick-header-columns-left', style: { left: '-1000px' } }, this._headerScrollerL ); this._headerR = createDomElement( 'div', { className: 'slick-header-columns slick-header-columns-right', style: { left: '-1000px' } }, this._headerScrollerR ); // Cache the header columns this._headers = [this._headerL, this._headerR]; this._headerRowScrollerL = createDomElement('div', { className: 'slick-headerrow slick-state-default' }, this._paneTopL); this._headerRowScrollerR = createDomElement('div', { className: 'slick-headerrow slick-state-default' }, this._paneTopR); this._headerRowScroller = [this._headerRowScrollerL, this._headerRowScrollerR]; this._headerRowSpacerL = createDomElement( 'div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._headerRowScrollerL ); this._headerRowSpacerR = createDomElement( 'div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._headerRowScrollerR ); this._headerRowL = createDomElement( 'div', { className: 'slick-headerrow-columns slick-headerrow-columns-left' }, this._headerRowScrollerL ); this._headerRowR = createDomElement( 'div', { className: 'slick-headerrow-columns slick-headerrow-columns-right' }, this._headerRowScrollerR ); this._headerRows = [this._headerRowL, this._headerRowR]; // Append the top panel scroller this._topPanelScrollerL = createDomElement('div', { className: 'slick-top-panel-scroller slick-state-default' }, this._paneTopL); this._topPanelScrollerR = createDomElement('div', { className: 'slick-top-panel-scroller slick-state-default' }, this._paneTopR); this._topPanelScrollers = [this._topPanelScrollerL, this._topPanelScrollerR]; // Append the top panel this._topPanelL = createDomElement('div', { className: 'slick-top-panel', style: { width: '10000px' } }, this._topPanelScrollerL); this._topPanelR = createDomElement('div', { className: 'slick-top-panel', style: { width: '10000px' } }, this._topPanelScrollerR); this._topPanels = [this._topPanelL, this._topPanelR]; if (!this._options.showColumnHeader) { this._headerScroller.forEach((el) => { Utils.hide(el); }); } if (!this._options.showTopPanel) { this._topPanelScrollers.forEach((scroller) => { Utils.hide(scroller); }); } if (!this._options.showHeaderRow) { this._headerRowScroller.forEach((scroller) => { Utils.hide(scroller); }); } // Append the viewport containers this._viewportTopL = createDomElement('div', { className: 'slick-viewport slick-viewport-top slick-viewport-left' }, this._paneTopL); this._viewportTopR = createDomElement('div', { className: 'slick-viewport slick-viewport-top slick-viewport-right' }, this._paneTopR); this._viewportBottomL = createDomElement( 'div', { className: 'slick-viewport slick-viewport-bottom slick-viewport-left' }, this._paneBottomL ); this._viewportBottomR = createDomElement( 'div', { className: 'slick-viewport slick-viewport-bottom slick-viewport-right' }, this._paneBottomR ); // Cache the viewports this._viewport = [this._viewportTopL, this._viewportTopR, this._viewportBottomL, this._viewportBottomR]; if (this._options.viewportClass) { this._viewport.forEach((view) => { view.classList.add(...classNameToList(this._options.viewportClass)); }); } // Default the active viewport to the top left this._activeViewportNode = this._viewportTopL; // Append the canvas containers this._canvasTopL = createDomElement('div', { className: 'grid-canvas grid-canvas-top grid-canvas-left' }, this._viewportTopL); this._canvasTopR = createDomElement('div', { className: 'grid-canvas grid-canvas-top grid-canvas-right' }, this._viewportTopR); this._canvasBottomL = createDomElement('div', { className: 'grid-canvas grid-canvas-bottom grid-canvas-left' }, this._viewportBottomL); this._canvasBottomR = createDomElement('div', { className: 'grid-canvas grid-canvas-bottom grid-canvas-right' }, this._viewportBottomR); // Cache the canvases this._canvas = [this._canvasTopL, this._canvasTopR, this._canvasBottomL, this._canvasBottomR]; this.scrollbarDimensions = this.scrollbarDimensions || this.measureScrollbar(); const canvasWithScrollbarWidth = this.getCanvasWidth() + this.scrollbarDimensions.width; // Default the active canvas to the top left this._activeCanvasNode = this._canvasTopL; // top-header if (this._topHeaderPanelSpacer) { Utils.width(this._topHeaderPanelSpacer, canvasWithScrollbarWidth); } // pre-header if (this._preHeaderPanelSpacer) { Utils.width(this._preHeaderPanelSpacer, canvasWithScrollbarWidth); } this._headers.forEach((el) => { Utils.width(el, this.getHeadersWidth()); }); Utils.width(this._headerRowSpacerL, canvasWithScrollbarWidth); Utils.width(this._headerRowSpacerR, canvasWithScrollbarWidth); // footer Row if (this._options.createFooterRow) { this._footerRowScrollerR = createDomElement('div', { className: 'slick-footerrow slick-state-default' }, this._paneTopR); this._footerRowScrollerL = createDomElement('div', { className: 'slick-footerrow slick-state-default' }, this._paneTopL); this._footerRowScroller = [this._footerRowScrollerL, this._footerRowScrollerR]; this._footerRowSpacerL = createDomElement( 'div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerL ); Utils.width(this._footerRowSpacerL, canvasWithScrollbarWidth); this._footerRowSpacerR = createDomElement( 'div', { style: { display: 'block', height: '1px', position: 'absolute', top: '0px', left: '0px' } }, this._footerRowScrollerR ); Utils.width(this._footerRowSpacerR, canvasWithScrollbarWidth); this._footerRowL = createDomElement( 'div', { className: 'slick-footerrow-columns slick-footerrow-columns-left' }, this._footerRowScrollerL ); this._footerRowR = createDomElement( 'div', { className: 'slick-footerrow-columns slick-footerrow-columns-right' }, this._footerRowScrollerR ); this._footerRow = [this._footerRowL, this._footerRowR]; if (!this._options.showFooterRow) { this._footerRowScroller.forEach((scroller) => { Utils.hide(scroller); }); } } this._focusSink2 = this._focusSink.cloneNode(true) as HTMLDivElement; this._container.appendChild(this._focusSink2); if (!this._options.explicitInitialization) { this.finishInitialization(); } } protected finishInitialization(): void { if (!this.initialized) { this.initialized = true; this.getViewportWidth(); this.getViewportHeight(); // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?) // calculate the diff so we can set consistent sizes this.measureCellPaddingAndBorder(); // disable all text selection in header (including input and textarea) this.disableSelection(this._headers); if (!this._options.enableTextSelectionOnCells) { // disable text selection in grid cells except in input and textarea elements this._viewport.forEach((view) => { this._bindingEventService.bind(view, 'selectstart', (event: Event) => { if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) { return; } event.preventDefault(); }); }); } this.setFrozenOptions(); this.setPaneFrozenClasses(); this.setPaneVisibility(); this.setScroller(); this.setOverflow(); this.updateColumnCaches(); this.createColumnHeaders(); this.createColumnFooter(); this.setupColumnSort(); this.createCssRules(); this.resizeCanvas(); this.bindAncestorScrollEvents(); this._bindingEventService.bind(this._container, 'resize', this.resizeCanvas.bind(this)); this._bindingEventService.bind(this._viewport, 'scroll', this.handleScroll.bind(this)); this._bindingEventService.bind(this._viewport, 'focus', () => { this._options.enableCellNavigation && this.focusGridCell(); }); if (this._options.enableMouseWheelScrollHandler) { this._viewport.forEach((view) => { this.slickMouseWheelInstances.push( MouseWheel({ element: view, onMouseWheel: this.handleMouseWheel.bind(this), }) ); }); } this._bindingEventService.bind(this._headerScroller, 'contextmenu', this.handleHeaderContextMenu.bind(this) as EventListener); this._bindingEventService.bind(this._headerScroller, 'click', this.handleHeaderClick.bind(this) as EventListener); this._bindingEventService.bind(this._headerRowScroller, 'scroll', this.handleHeaderRowScroll.bind(this) as EventListener); if (this._options.createFooterRow) { this._bindingEventService.bind(this._footerRow, 'contextmenu', this.handleFooterContextMenu.bind(this) as EventListener); this._bindingEventService.bind(this._footerRow, 'click', this.handleFooterClick.bind(this) as EventListener); this._bindingEventService.bind(this._footerRowScroller, 'scroll', this.handleFooterRowScroll.bind(this) as EventListener); } 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); this._bindingEventService.bind( [this._preHeaderPanelScroller, this._preHeaderPanelScrollerR], 'contextmenu', this.handlePreHeaderContextMenu.bind(this) as EventListener ); this._bindingEventService.bind( [this._preHeaderPanelScroller, this._preHeaderPanelScrollerR], 'click', this.handlePreHeaderClick.bind(this) as EventListener ); } this._bindingEventService.bind(this._focusSink, 'keydown', this.handleGridKeyDown.bind(this) as EventListener); this._bindingEventService.bind(this._focusSink2, 'keydown', this.handleGridKeyDown.bind(this) as EventListener); this._bindingEventService.bind(this._canvas, 'keydown', this.handleGridKeyDown.bind(this) as EventListener); this._bindingEventService.bind(this._canvas, 'click', this.handleClick.bind(this) as EventListener); this._bindingEventService.bind(this._canvas, 'dblclick', this.handleDblClick.bind(this) as EventListener); this._bindingEventService.bind(this._canvas, 'contextmenu', this.handleContextMenu.bind(this) as EventListener); this._bindingEventService.bind(this._canvas, 'mouseover', this.handleCellMouseOver.bind(this) as EventListener); this._bindingEventService.bind(this._canvas, 'mouseout', this.handleCellMouseOut.bind(this) as EventListener); this._bindingEventService.bind(this._container, 'keydown', this.handleContainerKeyDown.bind(this) as EventListener); if (Draggable) { this.slickDraggableInstance = Draggable({ containerElement: this._container, allowDragFrom: `div.slick-cell, div.${this.dragReplaceEl.cssClass}`, dragFromClassDetectArr: [{ tag: 'dragReplaceHandle', id: this.dragReplaceEl.id }], // the slick cell parent must always contain `.dnd` and/or `.cell-reorder` class to be identified as draggable allowDragFromClosest: this._options.allowDragFromClosest, preventDragFromKeys: this._options.preventDragFromKeys, onDragInit: this.handleDragInit.bind(this), onDragStart: this.handleDragStart.bind(this), onDrag: this.handleDrag.bind(this), onDragEnd: this.handleDragEnd.bind(this), }); } if (!this._options.suppressCssChangesOnHiddenInit) { this.restoreCssFromHiddenInit(); } } } /** handles "display:none" on container or container parents, related to issue: https://github.com/6pac/SlickGrid/issues/568 */ cacheCssForHiddenInit(): void { this._hiddenParents = Utils.parents(this._container, ':hidden') as HTMLElement[]; this.oldProps = []; this._hiddenParents.forEach((el) => { const old: Partial = {}; Object.keys(this.cssShow).forEach((name) => { if (this.cssShow) { old[name as any] = el.style[name as 'position' | 'visibility' | 'display']; el.style[name as any] = this.cssShow[name as 'position' | 'visibility' | 'display']; } }); this.oldProps.push(old); }); } restoreCssFromHiddenInit

>(): void { // finish handle display:none on container or container parents // - put values back the way they were let i = 0; if (this._hiddenParents) { this._hiddenParents.forEach((el) => { const old = this.oldProps[i++]; Object.keys(this.cssShow).forEach((name) => { if (this.cssShow) { (el.style as unknown as P)[name as keyof P] = (old as any)[name]; } }); }); this._hiddenParents.length = 0; } } protected hasFrozenColumns(): boolean { return this._options.frozenColumn! > -1; } /** Register an external Plugin */ registerPlugin(plugin: T): void { this.plugins.unshift(plugin); plugin.init(this as unknown as SlickGrid); } /** Unregister (destroy) an external Plugin */ unregisterPlugin(plugin: SlickPlugin): void { for (let i = this.plugins.length; i >= 0; i--) { if (this.plugins[i] === plugin) { this.plugins[i]?.destroy(); this.plugins.splice(i, 1); break; } } } /** Get a Plugin (addon) by its name */ getPluginByName

(name: string): P | undefined { for (let i = this.plugins.length - 1; i >= 0; i--) { if (this.plugins[i]?.pluginName === name) { return this.plugins[i] as P; } } return undefined; } getPubSubService(): BasePubSub | undefined { return this._pubSubService; } /** * Unregisters a current selection model and registers a new one. See the definition of SelectionModel for more information. * @param {Object} selectionModel A SelectionModel. */ setSelectionModel(model: SelectionModel): void { if (this.selectionModel) { this.selectionModel.onSelectedRangesChanged.unsubscribe(this.handleSelectedRangesChanged.bind(this)); this.selectionModel.destroy?.(); } this.selectionModel = model; if (this.selectionModel) { this.selectionModel.init(this as unknown as SlickGrid); this.selectionModel.onSelectedRangesChanged.subscribe(this.handleSelectedRangesChanged.bind(this)); } } /** Returns the current SelectionModel. See here for more information about SelectionModels. */ getSelectionModel(): T | undefined { return this.selectionModel as T; } /** Get Grid Canvas Node DOM Element */ getCanvasNode(columnIdOrIdx?: number | string, rowIndex?: number): HTMLDivElement { return this._getContainerElement(this.getCanvases(), columnIdOrIdx, rowIndex) as HTMLDivElement; } /** Get the canvas DOM element */ getActiveCanvasNode(e?: Event | SlickEventData): HTMLDivElement { if (e === undefined) { return this._activeCanvasNode; } if (e instanceof SlickEventData) { e = e.getNativeEvent(); } this._activeCanvasNode = (e as Event & { target: HTMLElement })?.target?.closest('.grid-canvas') as HTMLDivElement; return this._activeCanvasNode; } /** Get the canvas DOM element */ getCanvases(): HTMLDivElement[] { return this._canvas; } /** Get the Viewport DOM node element */ getViewportNode(columnIdOrIdx?: number | string, rowIndex?: number): HTMLElement | undefined { return this._getContainerElement(this.getViewports(), columnIdOrIdx, rowIndex); } /** Get all the Viewport node elements */ getViewports(): HTMLDivElement[] { return this._viewport; } getActiveViewportNode(e: Event | SlickEventData): HTMLDivElement { this.setActiveViewportNode(e); return this._activeViewportNode; } /** Sets an active viewport node */ setActiveViewportNode(e: Event | SlickEventData): HTMLDivElement { if (e instanceof SlickEventData) { e = e.getNativeEvent(); } this._activeViewportNode = (e as Event & { target: HTMLDivElement })?.target?.closest('.slick-viewport') as HTMLDivElement; return this._activeViewportNode; } protected _getContainerElement( targetContainers: HTMLElement[], columnIdOrIdx?: number | string, rowIndex?: number ): HTMLElement | undefined { if (!targetContainers) { return; } if (!columnIdOrIdx) { columnIdOrIdx = 0; } if (!rowIndex) { rowIndex = 0; } const idx = typeof columnIdOrIdx === 'number' ? columnIdOrIdx : this.getColumnIndex(columnIdOrIdx); const isBottomSide = this.hasFrozenRows && rowIndex >= this.actualFrozenRow + (this._options.frozenBottom ? 0 : 1); const isRightSide = this.hasFrozenColumns() && idx > this._options.frozenColumn!; return targetContainers[(isBottomSide ? 2 : 0) + (isRightSide ? 1 : 0)]; } protected measureScrollbar(): { width: number; height: number } { let className = ''; this._viewport.forEach((v) => (className += v.className)); const outerdiv = createDomElement( 'div', { className, style: { position: 'absolute', top: '-10000px', left: '-10000px', overflow: 'auto', width: '100px', height: '100px' }, }, document.body ); const innerdiv = createDomElement('div', { style: { width: '200px', height: '200px', overflow: 'auto' } }, outerdiv); const dim = { width: outerdiv.offsetWidth - outerdiv.clientWidth, height: outerdiv.offsetHeight - outerdiv.clientHeight, }; innerdiv.remove(); outerdiv.remove(); return dim; } /** Get the headers width in pixel */ getHeadersWidth(): number { this.headersWidth = this.headersWidthL = this.headersWidthR = 0; const includeScrollbar = !this._options.autoHeight; let i = 0; const ii = this.columns.length; for (i = 0; i < ii; i++) { if (!this.columns[i] || this.columns[i].hidden) { continue; } const width = this.columns[i].width; if (this._options.frozenColumn! > -1 && i > this._options.frozenColumn!) { this.headersWidthR += width || 0; } else { this.headersWidthL += width || 0; } } if (includeScrollbar) { if (this._options.frozenColumn! > -1 && i > this._options.frozenColumn!) { this.headersWidthR += this.scrollbarDimensions?.width || 0; } else { this.headersWidthL += this.scrollbarDimensions?.width || 0; } } if (this.hasFrozenColumns()) { this.headersWidthL = this.headersWidthL + 1000; this.headersWidthR = Math.max(this.headersWidthR, this.viewportW) + this.headersWidthL; this.headersWidthR += this.scrollbarDimensions?.width || 0; } else { this.headersWidthL += this.scrollbarDimensions?.width || 0; this.headersWidthL = Math.max(this.headersWidthL, this.viewportW) + 1000; } this.headersWidth = this.headersWidthL + this.headersWidthR; return Math.max(this.headersWidth, this.viewportW) + 1000; } /** Get the grid canvas width */ getCanvasWidth(): number { const availableWidth = this.getViewportInnerWidth(); let i = this.columns.length; this.canvasWidthL = this.canvasWidthR = 0; while (i--) { if (!this.columns[i] || this.columns[i].hidden) { continue; } if (this.hasFrozenColumns() && i > this._options.frozenColumn!) { this.canvasWidthR += this.columns[i].width || 0; } else { this.canvasWidthL += this.columns[i].width || 0; } } let totalRowWidth = this.canvasWidthL + this.canvasWidthR; if (this._options.fullWidthRows) { const extraWidth = Math.max(totalRowWidth, availableWidth) - totalRowWidth; if (extraWidth > 0) { totalRowWidth += extraWidth; if (this.hasFrozenColumns()) { this.canvasWidthR += extraWidth; } else { this.canvasWidthL += extraWidth; } } } return totalRowWidth; } /** * Validate that the column freeze is allowed in the browser by making sure that the frozen column is not exceeding the available and visible left canvas width. * Note that it will only validate when `invalidColumnFreezeWidthCallback` grid option is enabled. * @param {Number} frozenColumn the column index to freeze at * - if `undefined` it will do the condition check and never alert more than once * - if `true` it will do the condition check and always alert even if it was called before * - if `false` it will do the condition check but always skip the alert */ validateColumnFreezeWidth(frozenColumn = -1): boolean { if (frozenColumn >= 0) { let canvasWidthL = 0; this.columns.forEach((col, i) => { if (!col.hidden && i <= frozenColumn) { const { minWidth = 0, maxWidth = 0, width = this._options.defaultColumnWidth! } = col; let fwidth = width < minWidth ? minWidth : width; if (maxWidth > 0 && fwidth > maxWidth) { fwidth = maxWidth; } canvasWidthL += fwidth; } }); const cWidth = Utils.width(this._container) || 0; if (cWidth > 0 && canvasWidthL > cWidth && !this._options.skipFreezeColumnValidation) { if (this._options.invalidColumnFreezeWidthCallback) { this._options.invalidColumnFreezeWidthCallback?.(this._options.invalidColumnFreezeWidthMessage!); this._invalidfrozenAlerted = true; } return false; } } return true; } /** * Validate that the frozen column is allowed by verifying there is at least 1, or more, column to the right of the frozen column otherwise show an error * Note that it will only validate when `invalidColumnFreezePickerCallback` grid option is enabled. * @param {Number|String} [columnId] column id * @param {Boolean} [forceAlert] tri-state flag to alert when frozen column is invalid * @param {Array} [colums] optionally provide new columns to validate * - if `undefined` it will do the condition check and never alert more than once * - if `true` it will do the condition check and always alert even if it was called before * - if `false` it will do the condition check but always skip the alert */ validateColumnFreeze(columnId?: number | string, forceAlert = false, columns?: Column[]): boolean { const hasColummnIdArg = columnId !== undefined; columns ??= this.columns; if (columnId === undefined && (this._prevFrozenColumnIdx >= 0 || this._options.frozenColumn! >= 0)) { const column = columns[this._prevFrozenColumnIdx] ?? columns[this._options.frozenColumn!]; columnId = column?.id ?? ''; } const currentFrozenIdx = this._options.frozenColumn!; const frozenColumnId = currentFrozenIdx >= 0 && currentFrozenIdx <= columns.length ? columns[currentFrozenIdx].id : ''; if (!frozenColumnId || !columnId) { return true; } const visibleColumns = columns.filter((col) => !col.hidden); const colIdx = visibleColumns.findIndex((c) => c.id === columnId); const frozenColIdx = visibleColumns.findIndex((c) => c.id === frozenColumnId); let currentFrozenColumn = this._options.frozenColumn!; if ((frozenColIdx > colIdx && colIdx <= currentFrozenColumn) || currentFrozenColumn === -1) { return true; } if ( (currentFrozenColumn >= 0 && currentFrozenColumn >= visibleColumns.length - 2 && !this._options.skipFreezeColumnValidation) || (hasColummnIdArg && currentFrozenColumn === 0 && columnId !== undefined && this.columns[0].id === columnId) ) { if ((forceAlert !== false && !this._invalidfrozenAlerted) || forceAlert === true) { this._options.invalidColumnFreezePickerCallback?.(this._options.invalidColumnFreezePickerMessage!); this._invalidfrozenAlerted = true; } return false; } return true; } protected updateCanvasWidth(forceColumnWidthsUpdate?: boolean): void { const oldCanvasWidth = this.canvasWidth; const oldCanvasWidthL = this.canvasWidthL; 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) { Utils.width(this._canvasTopL, this.canvasWidthL); this.getHeadersWidth(); Utils.width(this._headerL, this.headersWidthL); Utils.width(this._headerR, this.headersWidthR); if (this.hasFrozenColumns()) { Utils.width(this._canvasTopR, this.canvasWidthR); Utils.width(this._paneHeaderL, this.canvasWidthL); Utils.setStyleSize(this._paneHeaderR, 'left', this.canvasWidthL); Utils.setStyleSize(this._paneHeaderR, 'width', this.viewportW - this.canvasWidthL); Utils.width(this._paneTopL, this.canvasWidthL); Utils.setStyleSize(this._paneTopR, 'left', this.canvasWidthL); Utils.width(this._paneTopR, this.viewportW - this.canvasWidthL); Utils.width(this._headerRowScrollerL, this.canvasWidthL); Utils.width(this._headerRowScrollerR, this.viewportW - this.canvasWidthL); Utils.width(this._headerRowL, this.canvasWidthL); Utils.width(this._headerRowR, this.canvasWidthR); if (this._options.createFooterRow) { Utils.width(this._footerRowScrollerL, this.canvasWidthL); Utils.width(this._footerRowScrollerR, this.viewportW - this.canvasWidthL); Utils.width(this._footerRowL, this.canvasWidthL); Utils.width(this._footerRowR, this.canvasWidthR); } if (this._options.createPreHeaderPanel) { Utils.width(this._preHeaderPanel, this._options.preHeaderPanelWidth ?? this.canvasWidth); } Utils.width(this._viewportTopL, this.canvasWidthL); Utils.width(this._viewportTopR, this.viewportW - this.canvasWidthL); if (this.hasFrozenRows) { Utils.width(this._paneBottomL, this.canvasWidthL); Utils.setStyleSize(this._paneBottomR, 'left', this.canvasWidthL); Utils.width(this._viewportBottomL, this.canvasWidthL); Utils.width(this._viewportBottomR, this.viewportW - this.canvasWidthL); Utils.width(this._canvasBottomL, this.canvasWidthL); Utils.width(this._canvasBottomR, this.canvasWidthR); } } else { Utils.width(this._paneHeaderL, '100%'); Utils.width(this._paneTopL, '100%'); Utils.width(this._headerRowScrollerL, '100%'); Utils.width(this._headerRowL, this.canvasWidth); if (this._options.createFooterRow) { Utils.width(this._footerRowScrollerL, '100%'); Utils.width(this._footerRowL, this.canvasWidth); } if (this._options.createPreHeaderPanel) { Utils.width(this._preHeaderPanel, this._options.preHeaderPanelWidth ?? this.canvasWidth); } Utils.width(this._viewportTopL, '100%'); if (this.hasFrozenRows) { Utils.width(this._viewportBottomL, '100%'); Utils.width(this._canvasBottomL, this.canvasWidthL); } } } this.viewportHasHScroll = this.canvasWidth >= this.viewportW - (this.scrollbarDimensions?.width || 0); Utils.width(this._headerRowSpacerL, this.canvasWidth + (this.viewportHasVScroll ? this.scrollbarDimensions?.width || 0 : 0)); Utils.width(this._headerRowSpacerR, this.canvasWidth + (this.viewportHasVScroll ? this.scrollbarDimensions?.width || 0 : 0)); if (this._options.createFooterRow) { Utils.width(this._footerRowSpacerL, this.canvasWidth + (this.viewportHasVScroll ? this.scrollbarDimensions?.width || 0 : 0)); Utils.width(this._footerRowSpacerR, this.canvasWidth + (this.viewportHasVScroll ? this.scrollbarDimensions?.width || 0 : 0)); } if (widthChanged || forceColumnWidthsUpdate) { this.applyColumnWidths(); } } protected disableSelection(target: HTMLElement[]): void { target.forEach((el) => { el.setAttribute('unselectable', 'on'); (el.style as any).mozUserSelect = 'none'; /* v8 ignore next */ this._bindingEventService.bind(el, 'selectstart', () => false); }); } protected getMaxSupportedCssHeight(): number { let supportedHeight = 1000000; // FF reports the height back but still renders blank after ~6M px // let testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000; const testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? this._options.ffMaxSupportedCssHeight : this._options.maxSupportedCssHeight; const div = createDomElement('div', { style: { display: 'hidden' } }, document.body); let condition = true; while (condition) { const test = supportedHeight * 2; Utils.height(div, test); const height = Utils.height(div); /* v8 ignore else */ if (test > testUpTo! || height !== test) { condition = false; break; } else { supportedHeight = test; } } div.remove(); return supportedHeight; } /** Get grid unique identifier */ getUID(): string { return this.uid; } /** Get Header Column Width Difference in pixel */ getHeaderColumnWidthDiff(): number { return this.headerColumnWidthDiff; } /** Get scrollbar dimensions */ getScrollbarDimensions(): { height: number; width: number } | undefined { return this.scrollbarDimensions; } /** Get the displayed scrollbar dimensions */ getDisplayedScrollbarDimensions(): { width: number; height: number } { return { width: this.viewportHasVScroll && this.scrollbarDimensions?.width ? this.scrollbarDimensions.width : 0, height: this.viewportHasHScroll && this.scrollbarDimensions?.height ? this.scrollbarDimensions.height : 0, }; } /** Get the absolute column minimum width */ getAbsoluteColumnMinWidth(): number { 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)); } } } /** * Updates an existing column definition and a corresponding header DOM element with the new title and tooltip. * @param {Number|String} columnId Column id. * @param {string | HTMLElement | DocumentFragment} [title] New column name. * @param {String} [toolTip] New column tooltip. */ updateColumnHeader(columnId: number | string, title?: string | HTMLElement | DocumentFragment, toolTip?: string): HTMLElement | void { if (this.initialized) { const idx = this.getColumnIndex(columnId); if (!isDefined(idx)) { return; } const columnDef = this.columns[idx]; const header: HTMLElement | undefined = this.getColumnHeaderByIndex(idx); if (header) { if (title !== undefined) { this.columns[idx].name = title; } if (toolTip !== undefined) { this.columns[idx].toolTip = toolTip; } this.triggerEvent(this.onBeforeHeaderCellDestroy, { node: header, column: columnDef, grid: this, }); header.setAttribute('title', toolTip || ''); if (title !== undefined) { applyHtmlToElement(header.children[0] as HTMLElement, title, this._options); } this.triggerEvent(this.onHeaderCellRendered, { node: header, column: columnDef, grid: this, }); } return header; } } /** * Get the Header DOM element * @param {C} columnDef - column definition */ getHeader(columnDef?: C): HTMLDivElement | HTMLDivElement[] { if (!columnDef) { return this.hasFrozenColumns() ? this._headers : this._headerL; } const idx = this.getColumnIndex(columnDef.id); return this.hasFrozenColumns() ? (idx <= this._options.frozenColumn! ? this._headerL : this._headerR) : this._headerL; } /** * Get a specific Header Column DOM element by its column Id or index * @param {Number|String} columnIdOrIdx - column Id or index */ getHeaderColumn(columnIdOrIdx: number | string): HTMLDivElement { const idx = typeof columnIdOrIdx === 'number' ? columnIdOrIdx : this.getColumnIndex(columnIdOrIdx); // prettier-ignore const targetHeader = this.hasFrozenColumns() ? ((idx <= this._options.frozenColumn!) ? this._headerL : this._headerR) : this._headerL; // prettier-ignore const targetIndex = this.hasFrozenColumns() ? ((idx <= this._options.frozenColumn!) ? idx : idx - this._options.frozenColumn! - 1) : idx; return targetHeader.children[targetIndex] as HTMLDivElement; } /** Get the Header Row DOM element */ getHeaderRow(): HTMLDivElement | HTMLDivElement[] { return this.hasFrozenColumns() ? this._headerRows : this._headerRows?.[0]; } /** Get the Footer DOM element */ getFooterRow(): HTMLDivElement | HTMLDivElement[] { return this.hasFrozenColumns() ? this._footerRow : this._footerRow?.[0]; } /** @alias `getPreHeaderPanelLeft` */ getPreHeaderPanel(): HTMLDivElement { return this._preHeaderPanel; } /** Get the Pre-Header Panel Left DOM node element */ getPreHeaderPanelLeft(): HTMLDivElement { return this._preHeaderPanel; } /** Get the Pre-Header Panel Right DOM node element */ getPreHeaderPanelRight(): HTMLDivElement { return this._preHeaderPanelR; } /** Get the Top-Header Panel DOM node element */ getTopHeaderPanel(): HTMLDivElement { return this._topHeaderPanel; } /** * Get Header Row Column DOM element by its column Id or index * @param {Number|String} columnIdOrIdx - column Id or index */ getHeaderRowColumn(columnIdOrIdx: number | string): HTMLDivElement { let idx = typeof columnIdOrIdx === 'number' ? columnIdOrIdx : this.getColumnIndex(columnIdOrIdx); let headerRowTarget: HTMLDivElement; if (this.hasFrozenColumns()) { if (idx <= this._options.frozenColumn!) { headerRowTarget = this._headerRowL; } else { headerRowTarget = this._headerRowR; idx -= this._options.frozenColumn! + 1; } } else { headerRowTarget = this._headerRowL; } return headerRowTarget.children[idx] as HTMLDivElement; } /** * Get the Footer Row Column DOM element by its column Id or index * @param {Number|String} columnIdOrIdx - column Id or index */ getFooterRowColumn(columnIdOrIdx: number | string): HTMLDivElement { let idx = typeof columnIdOrIdx === 'number' ? columnIdOrIdx : this.getColumnIndex(columnIdOrIdx); let footerRowTarget: HTMLDivElement | null; if (this.hasFrozenColumns()) { if (idx <= this._options.frozenColumn!) { footerRowTarget = this._footerRowL; } else { footerRowTarget = this._footerRowR; idx -= this._options.frozenColumn! + 1; } } else { footerRowTarget = this._footerRowL; } return footerRowTarget?.children[idx] as HTMLDivElement; } protected createColumnFooter(): void { if (this._options.createFooterRow) { this._footerRow.forEach((footer) => { const columnElements = footer.querySelectorAll('.slick-footerrow-column'); columnElements.forEach((column) => { const columnDef = Utils.storage.get(column, 'column'); this.triggerEvent(this.onBeforeFooterRowCellDestroy, { node: column, column: columnDef, grid: this, }); }); }); emptyElement(this._footerRowL); emptyElement(this._footerRowR); for (let i = 0; i < this.columns.length; i++) { const m = this.columns[i]; if (!m || m.hidden) { continue; } const footerRowCell = createDomElement( 'div', { className: `slick-state-default slick-footerrow-column l${i} r${i}` }, this.hasFrozenColumns() && i > this._options.frozenColumn! ? this._footerRowR : this._footerRowL ); const className = this.hasFrozenColumns() && i <= this._options.frozenColumn! ? 'frozen' : null; if (className) { footerRowCell.classList.add(className); } Utils.storage.put(footerRowCell, 'column', m); this.triggerEvent(this.onFooterRowCellRendered, { node: footerRowCell, column: m, grid: this, }); } } } protected handleHeaderMouseHoverOn(e: Event | SlickEventData): void { (e as any)?.target.classList.add('slick-state-hover'); } protected handleHeaderMouseHoverOff(e: Event | SlickEventData): void { (e as any)?.target.classList.remove('slick-state-hover'); } protected createColumnHeaders(): void { this._bindingEventService.unbindAll('colheaders'); this._headers.forEach((header) => { const columnElements = header.querySelectorAll('.slick-header-column'); columnElements.forEach((column) => { const columnDef = Utils.storage.get(column, 'column'); if (columnDef) { this.triggerEvent(this.onBeforeHeaderCellDestroy, { node: column, column: columnDef, grid: this, }); } }); }); emptyElement(this._headerL); emptyElement(this._headerR); this.getHeadersWidth(); Utils.width(this._headerL, this.headersWidthL); Utils.width(this._headerR, this.headersWidthR); this._headerRows.forEach((row) => { const columnElements = row.querySelectorAll('.slick-headerrow-column'); columnElements.forEach((column) => { const columnDef = Utils.storage.get(column, 'column'); if (columnDef) { this.triggerEvent(this.onBeforeHeaderRowCellDestroy, { node: this, column: columnDef, grid: this, }); } }); }); emptyElement(this._headerRowL); emptyElement(this._headerRowR); if (this._options.createFooterRow) { const footerRowLColumnElements = this._footerRowL.querySelectorAll('.slick-footerrow-column'); footerRowLColumnElements.forEach((column) => { const columnDef = Utils.storage.get(column, 'column'); if (columnDef) { this.triggerEvent(this.onBeforeFooterRowCellDestroy, { node: this, column: columnDef, grid: this, }); } }); emptyElement(this._footerRowL); if (this.hasFrozenColumns()) { const footerRowRColumnElements = this._footerRowR.querySelectorAll('.slick-footerrow-column'); footerRowRColumnElements.forEach((column) => { const columnDef = Utils.storage.get(column, 'column'); if (columnDef) { this.triggerEvent(this.onBeforeFooterRowCellDestroy, { node: this, column: columnDef, grid: this, }); } }); emptyElement(this._footerRowR); } } for (let i = 0; i < this.columns.length; i++) { const m: C = this.columns[i]; if (!m || m.hidden) { continue; } const headerTarget = this.hasFrozenColumns() ? (i <= this._options.frozenColumn! ? this._headerL : this._headerR) : this._headerL; const headerRowTarget = this.hasFrozenColumns() ? i <= this._options.frozenColumn! ? this._headerRowL : this._headerRowR : this._headerRowL; const header = createDomElement( 'div', { id: `${this.uid + m.id}`, dataset: { id: String(m.id) }, role: 'columnheader', className: 'slick-state-default slick-header-column', tabIndex: 0, }, headerTarget ); if (m.toolTip) { header.title = m.toolTip; } if (!m.reorderable) { header.classList.add(this._options.unorderableColumnCssClass!); } const colNameElm = createDomElement('span', { className: 'slick-column-name' }, header); applyHtmlToElement(colNameElm, m.name, this._options); Utils.width(header, m.width! - this.headerColumnWidthDiff); let classname = m.headerCssClass || null; if (classname) { header.classList.add(...classNameToList(classname)); } classname = this.hasFrozenColumns() && i <= this._options.frozenColumn! ? 'frozen' : null; if (classname) { header.classList.add(classname); } this._bindingEventService.bind(header, 'mouseenter', this.handleHeaderMouseEnter.bind(this) as EventListener, {}, 'colheaders'); this._bindingEventService.bind(header, 'mouseleave', this.handleHeaderMouseLeave.bind(this) as EventListener, {}, 'colheaders'); this._bindingEventService.bind(header, 'mouseover', this.handleHeaderMouseOver.bind(this) as EventListener, {}, 'colheaders'); this._bindingEventService.bind(header, 'mouseout', this.handleHeaderMouseOut.bind(this) as EventListener, {}, 'colheaders'); Utils.storage.put(header, 'column', m); if (this._options.enableColumnReorder || m.sortable) { this._bindingEventService.bind(header, 'mouseenter', this.handleHeaderMouseHoverOn.bind(this) as EventListener, {}, 'colheaders'); this._bindingEventService.bind(header, 'mouseleave', this.handleHeaderMouseHoverOff.bind(this) as EventListener, {}, 'colheaders'); } if (m.hasOwnProperty('headerCellAttrs') && m.headerCellAttrs instanceof Object) { Object.keys(m.headerCellAttrs).forEach((key) => { if (m.headerCellAttrs.hasOwnProperty(key)) { header.setAttribute(key, m.headerCellAttrs[key]); } }); } if (m.sortable) { header.classList.add('slick-header-sortable'); createDomElement( 'div', { className: `slick-sort-indicator ${this._options.numberedMultiColumnSort && !this._options.sortColNumberInSeparateSpan ? ' slick-sort-indicator-numbered' : ''}`, }, header ); if (this._options.numberedMultiColumnSort && this._options.sortColNumberInSeparateSpan) { createDomElement('div', { className: 'slick-sort-indicator-numbered' }, header); } } this.triggerEvent(this.onHeaderCellRendered, { node: header, column: m, grid: this, }); if (this._options.showHeaderRow) { const headerRowCell = createDomElement( 'div', { className: `slick-state-default slick-headerrow-column l${i} r${i}` }, headerRowTarget ); const frozenClasses = this.hasFrozenColumns() && i <= this._options.frozenColumn! ? 'frozen' : null; if (frozenClasses) { headerRowCell.classList.add(frozenClasses); } // prettier-ignore this._bindingEventService.bind(headerRowCell, 'mouseenter', this.handleHeaderRowMouseEnter.bind(this) as EventListener, {}, 'colheaders'); // prettier-ignore this._bindingEventService.bind(headerRowCell, 'mouseleave', this.handleHeaderRowMouseLeave.bind(this) as EventListener, {}, 'colheaders'); // prettier-ignore this._bindingEventService.bind(headerRowCell, 'mouseover', this.handleHeaderRowMouseOver.bind(this) as EventListener, {}, 'colheaders'); this._bindingEventService.bind( headerRowCell, 'mouseout', this.handleHeaderRowMouseOut.bind(this) as EventListener, {}, 'colheaders' ); Utils.storage.put(headerRowCell, 'column', m); this.triggerEvent(this.onHeaderRowCellRendered, { node: headerRowCell, column: m, grid: this, }); } if (this._options.createFooterRow && this._options.showFooterRow) { const footerRowTarget = this.hasFrozenColumns() ? i <= this._options.frozenColumn! ? this._footerRow[0] : this._footerRow[1] : this._footerRow[0]; const footerRowCell = createDomElement( 'div', { className: `slick-state-default slick-footerrow-column l${i} r${i}` }, footerRowTarget ); Utils.storage.put(footerRowCell, 'column', m); this.triggerEvent(this.onFooterRowCellRendered, { node: footerRowCell, column: m, grid: this, }); } } this.setSortColumns(this.sortColumns); this.setupColumnResize(); if (this._options.enableColumnReorder) { if (typeof this._options.enableColumnReorder === 'function') { this._options.enableColumnReorder( this as unknown as SlickGrid, this._headers, this.headerColumnWidthDiff, this.setColumns as any, this.setupColumnResize, this.columns, this.getColumnIndex, this.uid, this.triggerEvent ); } else { this.setupColumnReorder(); } } } protected setupColumnSort(): void { this._bindingEventService.unbindAll('colsorts'); this._headers.forEach((header) => { const sortCallback = (e: (MouseEvent | KeyboardEvent) & { target: HTMLElement }) => { if (this.columnResizeDragging || e.target.classList.contains('slick-resizable-handle')) { return; } const coll = e.target.closest('.slick-header-column'); if (!coll) { return; } const column = Utils.storage.get(coll, 'column'); if (column?.sortable) { if (!this.getEditorLock()?.commitCurrentEdit()) { return; } const previousSortColumns = this.sortColumns.slice(); let sortColumn: ColumnSort | null = null; let i = 0; for (; i < this.sortColumns.length; i++) { if (this.sortColumns[i].columnId === column.id) { sortColumn = this.sortColumns[i]; sortColumn.sortAsc = !sortColumn.sortAsc; break; } } const hadSortCol = !!sortColumn; if (this._options.tristateMultiColumnSort) { if (!sortColumn) { sortColumn = { columnId: column.id, sortAsc: column.defaultSortAsc, sortCol: column }; } if (hadSortCol && sortColumn.sortAsc) { // three state: remove sort rather than go back to ASC this.sortColumns.splice(i, 1); sortColumn = null; } if (!this._options.multiColumnSort) { this.sortColumns = []; } if (sortColumn && (!hadSortCol || !this._options.multiColumnSort)) { this.sortColumns.push(sortColumn); } } else { // legacy behaviour if (e.metaKey && this._options.multiColumnSort) { if (sortColumn) { this.sortColumns.splice(i, 1); } } else { if ((!e.shiftKey && !e.metaKey) || !this._options.multiColumnSort) { this.sortColumns = []; } if (!sortColumn) { sortColumn = { columnId: column.id, sortAsc: column.defaultSortAsc, sortCol: column }; this.sortColumns.push(sortColumn); } else if (this.sortColumns.length === 0) { this.sortColumns.push(sortColumn); } } } let onSortArgs; if (!this._options.multiColumnSort) { onSortArgs = { multiColumnSort: false, previousSortColumns, columnId: this.sortColumns.length > 0 ? column.id : null, sortCol: this.sortColumns.length > 0 ? column : null, sortAsc: this.sortColumns.length > 0 ? this.sortColumns[0].sortAsc : true, }; } else { onSortArgs = { multiColumnSort: true, previousSortColumns, sortCols: this.sortColumns .map((col) => { const tempCol = this.getColumnById(col.columnId); return tempCol && !tempCol.hidden ? { columnId: tempCol.id, sortCol: tempCol, sortAsc: col.sortAsc } : null; }) .filter((el) => el), }; } if (this.triggerEvent(this.onBeforeSort, onSortArgs, e).getReturnValue() !== false) { this.setSortColumns(this.sortColumns); this.triggerEvent(this.onSort, onSortArgs, e); } } }; // Add keydown/click event handlers for sortable columns this._bindingEventService.bind( header, 'keydown', ((e: KeyboardEvent & { target: HTMLElement }) => { this.triggerEvent(this.onHeaderKeyDown, { event: e, column: Utils.storage.get(e.target, 'column'), grid: this }); if (e.key === 'Enter' || e.key === ' ') { sortCallback(e); } }) as EventListener, {}, 'colsorts' ); this._bindingEventService.bind( header, 'click', ((e: MouseEvent & { target: HTMLElement }) => sortCallback(e)) as EventListener, {}, 'colsorts' ); }); } protected setupColumnReorder(): void { this.sortableSideLeftInstance?.destroy(); this.sortableSideRightInstance?.destroy(); let columnScrollTimer: any; // add/remove extra scroll padding for calculation const scrollColumnsRight = () => (this._viewportScrollContainerX.scrollLeft += 10); const scrollColumnsLeft = () => (this._viewportScrollContainerX.scrollLeft -= 10); let prevColumnIds: Array = []; let columnMap: Map; let canDragScroll = false; const sortableOptions = { animation: 50, direction: 'horizontal', ghostClass: 'slick-sortable-placeholder', draggable: '.slick-header-column', dragoverBubble: false, preventOnFilter: false, // allow column to be resized even when they are not orderable revertClone: true, scroll: !this.hasFrozenColumns(), // enable auto-scroll // lock unorderable columns by using a combo of filter + onMove filter: `.${this._options.unorderableColumnCssClass}`, onMove: (event) => { return !event.related.classList.contains(this._options.unorderableColumnCssClass as string); }, onStart: (e) => { e.item.classList.add('slick-header-column-active'); canDragScroll = !this.hasFrozenColumns() || getOffset(e.item).left > getOffset(this._viewportScrollContainerX).left; if (canDragScroll && (e as SortableEvent & { originalEvent: MouseEvent }).originalEvent.pageX > this._container.clientWidth) { if (!columnScrollTimer) { columnScrollTimer = setInterval(scrollColumnsRight, 100); } } else if ( canDragScroll && (e as SortableEvent & { originalEvent: MouseEvent }).originalEvent.pageX < getOffset(this._viewportScrollContainerX).left ) { if (!columnScrollTimer) { columnScrollTimer = setInterval(scrollColumnsLeft, 100); } } else { clearInterval(columnScrollTimer); } prevColumnIds = this.columns.map((c) => c.id); // Create a map to track original column positions and hidden state columnMap = new Map(); this.columns.forEach((col, idx) => { columnMap.set(col.id, { index: idx, hidden: !!col.hidden, column: col }); }); }, onEnd: (e) => { e.item.classList.remove('slick-header-column-active'); clearInterval(columnScrollTimer); const prevScrollLeft = this.scrollLeft; if (!this.getEditorLock()?.commitCurrentEdit()) { return; } let reorderedIds = this.sortableSideLeftInstance?.toArray() ?? []; reorderedIds = reorderedIds.concat(this.sortableSideRightInstance?.toArray() ?? []); const reorderedColumns: C[] = []; for (let i = 0; i < reorderedIds.length; i++) { reorderedColumns.push(this.columns[this.getColumnIndex(reorderedIds[i])]); } // Reconstruct final column array: insert hidden columns at their original indices const finalColumns: C[] = []; let visibleIdx = 0; for (let i = 0; i < this.columns.length; i++) { const colInfo = columnMap.get(this.columns[i].id); if (colInfo?.hidden) { // Hidden column: insert at its original position finalColumns.push(colInfo.column); } else { // Visible column: use reordered position finalColumns.push(reorderedColumns[visibleIdx++]); } } e.stopPropagation(); if (!this.arrayEquals(prevColumnIds, reorderedIds)) { this.setColumns(finalColumns); // reapply previous scroll position since it might move back to x=0 after calling `setColumns()` (especially when `frozenColumn` is set) this.scrollToX(prevScrollLeft); this.triggerEvent(this.onColumnsReordered, { impactedColumns: this.columns, previousColumnOrder: prevColumnIds }); this.setupColumnResize(); } if (this.activeCellNode) { this.setFocus(); // refocus on active cell } }, } as SortableOptions; this.sortableSideLeftInstance = Sortable.create(this._headerL, sortableOptions); this.sortableSideRightInstance = Sortable.create(this._headerR, sortableOptions); } protected getHeaderChildren(): HTMLElement[] { const a = Array.from(this._headers[0].children); const b = Array.from(this._headers[1].children); return a.concat(b) as HTMLElement[]; } protected handleResizeableDoubleClick(evt: MouseEvent & { target: HTMLDivElement }): void { const triggeredByColumn = evt.target.parentElement!.id.replace(this.uid, ''); this.triggerEvent(this.onColumnsResizeDblClick, { triggeredByColumn }); } protected setupColumnResize(): void { let j: number; let k: number; let c: C; let pageX: number; let minPageX: number; let maxPageX: number; let firstResizable: number | undefined; let lastResizable = -1; let frozenLeftColMaxWidth = 0; this._bindingEventService.unbindAll('colresizes'); const children: HTMLElement[] = this.getHeaderChildren(); const vc = this.getVisibleColumns(); for (let i = 0; i < children.length; i++) { const child = children[i]; const handles = child.querySelectorAll('.slick-resizable-handle'); handles.forEach((handle) => handle.remove()); if (i < vc.length && vc[i]?.resizable) { if (firstResizable === undefined) { firstResizable = i; } lastResizable = i; } } if (firstResizable === undefined) { return; } for (let i = 0; i < children.length; i++) { const colElm = children[i]; /* v8 ignore if */ if (i >= vc.length || !vc[i]) { continue; } if (i < firstResizable || (this._options.forceFitColumns && i >= lastResizable)) { continue; } const resizeableHandle = createDomElement( 'div', { className: 'slick-resizable-handle', role: 'separator', ariaOrientation: 'horizontal' }, colElm ); this._bindingEventService.bind( resizeableHandle, 'dblclick', this.handleResizeableDoubleClick.bind(this) as EventListener, {}, 'colresizes' ); this.slickResizableInstances.push( Resizable({ resizeableElement: colElm as HTMLElement, resizeableHandleElement: resizeableHandle, onResizeStart: (e, resizeElms): boolean | void => { const targetEvent = (e as TouchEvent).touches ? (e as TouchEvent).changedTouches[0] : e; if (!this.getEditorLock()?.commitCurrentEdit()) { return false; } pageX = (targetEvent as MouseEvent).pageX; frozenLeftColMaxWidth = 0; resizeElms.resizeableElement.classList.add('slick-header-column-active'); let shrinkLeewayOnRight: number | null = null; let stretchLeewayOnRight: number | null = null; // lock each column's width option to current width for (let pw = 0; pw < children.length; pw++) { if (pw < vc.length && vc[pw]) { vc[pw].previousWidth = children[pw].offsetWidth; } } if (this._options.forceFitColumns) { shrinkLeewayOnRight = 0; stretchLeewayOnRight = 0; // colums on right affect maxPageX/minPageX for (j = i + 1; j < vc.length; j++) { c = vc[j]; if (c?.resizable) { if (stretchLeewayOnRight !== null) { if (c.maxWidth) { stretchLeewayOnRight += c.maxWidth - (c.previousWidth || 0); } else { stretchLeewayOnRight = null; } } shrinkLeewayOnRight += (c.previousWidth || 0) - Math.max(c.minWidth || 0, this.absoluteColumnMinWidth); } } } let shrinkLeewayOnLeft = 0; let stretchLeewayOnLeft: number | null = 0; for (j = 0; j <= i; j++) { // columns on left only affect minPageX c = vc[j]; if (c?.resizable) { if (stretchLeewayOnLeft !== null) { /* v8 ignore if */ if (c.maxWidth) { stretchLeewayOnLeft += c.maxWidth - (c.previousWidth || 0); } else { stretchLeewayOnLeft = null; } } shrinkLeewayOnLeft += (c.previousWidth || 0) - Math.max(c.minWidth || 0, this.absoluteColumnMinWidth); } } maxPageX = pageX + Math.min(shrinkLeewayOnRight ?? 100000, stretchLeewayOnLeft ?? 100000); minPageX = pageX - Math.min(shrinkLeewayOnLeft ?? 100000, stretchLeewayOnRight ?? 100000); }, onResize: (e, resizeElms) => { const targetEvent = (e as TouchEvent).touches ? (e as TouchEvent).changedTouches[0] : e; this.columnResizeDragging = true; let actualMinWidth; const targetPageX = (targetEvent as MouseEvent).pageX; const d = Math.min(maxPageX, Math.max(minPageX, targetPageX)) - pageX; let x; let newCanvasWidthL = 0; // oxlint-disable-next-line no-unused-vars let newCanvasWidthR = 0; const viewportWidth = this.getViewportInnerWidth(); if (d < 0) { // shrink column x = d; for (j = i; j >= 0; j--) { c = vc[j]; if (c && c.resizable && !c.hidden) { actualMinWidth = Math.max(c.minWidth || 0, this.absoluteColumnMinWidth); /* v8 ignore if */ if (x && (c.previousWidth || 0) + x < actualMinWidth) { x += (c.previousWidth || 0) - actualMinWidth; c.width = actualMinWidth; } else { c.width = (c.previousWidth || 0) + x; x = 0; } } } for (k = 0; k <= i; k++) { c = vc[k]; if (c && !c.hidden) { if (this.hasFrozenColumns() && k > this._options.frozenColumn!) { newCanvasWidthR += c.width || 0; } else { newCanvasWidthL += c.width || 0; } } } if (this._options.forceFitColumns) { x = -d; for (j = i + 1; j < vc.length; j++) { c = vc[j]; if (c && !c.hidden) { if (c.resizable) { if (x && c.maxWidth && c.maxWidth - (c.previousWidth || 0) < x) { x -= c.maxWidth - (c.previousWidth || 0); c.width = c.maxWidth; } else { c.width = (c.previousWidth || 0) + x; x = 0; } if (this.hasFrozenColumns() && j > this._options.frozenColumn!) { newCanvasWidthR += c.width || 0; } else { newCanvasWidthL += c.width || 0; } } } } } else { for (j = i + 1; j < vc.length; j++) { c = vc[j]; if (c && !c.hidden) { if (this.hasFrozenColumns() && j > this._options.frozenColumn!) { newCanvasWidthR += c.width || 0; } else { newCanvasWidthL += c.width || 0; } } } } if (this._options.forceFitColumns) { x = -d; for (j = i + 1; j < vc.length; j++) { c = vc[j]; if (c && !c.hidden && c.resizable) { /* v8 ignore if */ if (x && c.maxWidth && c.maxWidth - (c.previousWidth || 0) < x) { x -= c.maxWidth - (c.previousWidth || 0); c.width = c.maxWidth; } else { c.width = (c.previousWidth || 0) + x; x = 0; } } } } } else { // stretch column x = d; newCanvasWidthL = 0; newCanvasWidthR = 0; for (j = i; j >= 0; j--) { c = vc[j]; if (c && !c.hidden && c.resizable) { if (x && c.maxWidth && c.maxWidth - (c.previousWidth || 0) < x) { x -= c.maxWidth - (c.previousWidth || 0); c.width = c.maxWidth; } else { const newWidth = (c.previousWidth || 0) + x; const resizedCanvasWidthL = this.canvasWidthL + x; if (this.hasFrozenColumns() && j <= this._options.frozenColumn!) { // if we're on the left frozen side, we need to make sure that our left section width never goes over the total viewport width // prettier-ignore if (newWidth > frozenLeftColMaxWidth && resizedCanvasWidthL < viewportWidth - this._options.frozenRightViewportMinWidth!) { frozenLeftColMaxWidth = newWidth; // keep max column width ref, if we go over the limit this number will stop increasing } // prettier-ignore c.width = resizedCanvasWidthL + this._options.frozenRightViewportMinWidth! > viewportWidth ? frozenLeftColMaxWidth : newWidth; } else { c.width = newWidth; } x = 0; } } } for (k = 0; k <= i; k++) { c = vc[k]; if (c && !c.hidden) { if (this.hasFrozenColumns() && k > this._options.frozenColumn!) { newCanvasWidthR += c.width || 0; } else { newCanvasWidthL += c.width || 0; } } } if (this._options.forceFitColumns) { x = -d; for (j = i + 1; j < vc.length; j++) { c = vc[j]; if (c && !c.hidden && c.resizable) { actualMinWidth = Math.max(c.minWidth || 0, this.absoluteColumnMinWidth); /* v8 ignore if */ if (x && (c.previousWidth || 0) + x < actualMinWidth) { x += (c.previousWidth || 0) - actualMinWidth; c.width = actualMinWidth; } else { c.width = (c.previousWidth || 0) + x; x = 0; } if (this.hasFrozenColumns() && j > this._options.frozenColumn!) { newCanvasWidthR += c.width || 0; } else { newCanvasWidthL += c.width || 0; } } } } else { for (j = i + 1; j < vc.length; j++) { c = vc[j]; if (c && !c.hidden) { if (this.hasFrozenColumns() && j > this._options.frozenColumn!) { // eslint-disable-next-line newCanvasWidthR += c.width || 0; } else { newCanvasWidthL += c.width || 0; } } } } } if (this.hasFrozenColumns() && newCanvasWidthL !== this.canvasWidthL) { Utils.width(this._headerL, newCanvasWidthL + 1000); Utils.setStyleSize(this._paneHeaderR, 'left', newCanvasWidthL); } this.applyColumnHeaderWidths(); if (this._options.syncColumnCellResize) { this.applyColumnWidths(); } this.triggerEvent(this.onColumnsDrag, { triggeredByColumn: resizeElms.resizeableElement, resizeHandle: resizeElms.resizeableHandleElement, }); }, onResizeEnd: (_e, resizeElms) => { resizeElms.resizeableElement.classList.remove('slick-header-column-active'); const triggeredByColumn = resizeElms.resizeableElement.id.replace(this.uid, ''); if (this.triggerEvent(this.onBeforeColumnsResize, { triggeredByColumn }).getReturnValue() === true) { this.applyColumnHeaderWidths(); } let newWidth; for (j = 0; j < vc.length; j++) { c = vc[j]; if (c && !c.hidden && children[j]) { newWidth = children[j].offsetWidth; if (c.previousWidth !== newWidth && c.rerenderOnResize) { this.invalidateAllRows(); } } } this.updateCanvasWidth(true); this.render(); this.triggerEvent(this.onColumnsResized, { triggeredByColumn }); clearTimeout(this._columnResizeTimer); this._columnResizeTimer = setTimeout(() => (this.columnResizeDragging = false), this._options.columnResizingDelay); }, }) ); } } /** * Calculates the vertical box sizes (the sum of top/bottom borders and paddings) * for a given element by reading its computed style. * @param el * @returns number */ protected getVBoxDelta(el: HTMLElement): number { const p = ['borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom']; const styles = getComputedStyle(el); let delta = 0; p.forEach((val) => (delta += Utils.toFloat(styles[val as any]))); return delta; } protected setFrozenOptions(): void { this._options.frozenColumn = this._options.frozenColumn! >= 0 && this._options.frozenColumn! < this.columns.length ? parseInt(this._options.frozenColumn as unknown as string, 10) : -1; if (this._options.frozenRow! > -1) { this.hasFrozenRows = true; this.frozenRowsHeight = this._options.frozenRow! * this._options.rowHeight!; const dataLength = this.getDataLength(); this.actualFrozenRow = this._options.frozenBottom ? dataLength - this._options.frozenRow! : this._options.frozenRow!; } else { this.hasFrozenRows = false; } } /** add/remove frozen class to left headers/footer when defined */ protected setPaneFrozenClasses(): void { const classAction = this.hasFrozenColumns() ? 'add' : 'remove'; for (const elm of [this._paneHeaderL, this._paneTopL, this._paneBottomL]) { elm.classList[classAction]('frozen'); } } protected setPaneVisibility(): void { if (this.hasFrozenColumns()) { Utils.show(this._paneHeaderR); Utils.show(this._paneTopR); if (this.hasFrozenRows) { Utils.show(this._paneBottomL); Utils.show(this._paneBottomR); } else { Utils.hide(this._paneBottomR); Utils.hide(this._paneBottomL); } } else { Utils.hide(this._paneHeaderR); Utils.hide(this._paneTopR); Utils.hide(this._paneBottomR); if (this.hasFrozenRows) { Utils.show(this._paneBottomL); } else { Utils.hide(this._paneBottomR); Utils.hide(this._paneBottomL); } } } protected setOverflow(): void { this._viewportTopL.style.overflowX = this.hasFrozenColumns() ? this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'hidden' : 'scroll' : this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'hidden' : 'auto'; this._viewportTopL.style.overflowY = !this.hasFrozenColumns() && this._options.alwaysShowVerticalScroll ? 'scroll' : this.hasFrozenColumns() ? this.hasFrozenRows ? 'hidden' : 'hidden' : this.hasFrozenRows ? 'scroll' : 'auto'; this._viewportTopR.style.overflowX = this.hasFrozenColumns() ? this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'hidden' : 'scroll' : this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'hidden' : 'auto'; this._viewportTopR.style.overflowY = this._options.alwaysShowVerticalScroll ? 'scroll' : this.hasFrozenColumns() ? this.hasFrozenRows ? 'scroll' : 'auto' : this.hasFrozenRows ? 'scroll' : 'auto'; this._viewportBottomL.style.overflowX = this.hasFrozenColumns() ? this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'scroll' : 'auto' : this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'auto' : 'auto'; this._viewportBottomL.style.overflowY = !this.hasFrozenColumns() && this._options.alwaysShowVerticalScroll ? 'scroll' : this.hasFrozenColumns() ? this.hasFrozenRows ? 'hidden' : 'hidden' : this.hasFrozenRows ? 'scroll' : 'auto'; this._viewportBottomR.style.overflowX = this.hasFrozenColumns() ? this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'scroll' : 'auto' : this.hasFrozenRows && !this._options.alwaysAllowHorizontalScroll ? 'auto' : 'auto'; this._viewportBottomR.style.overflowY = this._options.alwaysShowVerticalScroll ? 'scroll' : this.hasFrozenColumns() ? this.hasFrozenRows ? 'auto' : 'auto' : this.hasFrozenRows ? 'auto' : 'auto'; if (this._options.viewportClass) { const viewportClasses = classNameToList(this._options.viewportClass); this._viewportTopL.classList.add(...viewportClasses); this._viewportTopR.classList.add(...viewportClasses); this._viewportBottomL.classList.add(...viewportClasses); this._viewportBottomR.classList.add(...viewportClasses); } } protected setScroller(): void { if (this.hasFrozenColumns()) { this._headerScrollContainer = this._headerScrollerR; this._headerRowScrollContainer = this._headerRowScrollerR; this._footerRowScrollContainer = this._footerRowScrollerR; if (this.hasFrozenRows) { if (this._options.frozenBottom) { this._viewportScrollContainerX = this._viewportBottomR; this._viewportScrollContainerY = this._viewportTopR; } else { this._viewportScrollContainerX = this._viewportScrollContainerY = this._viewportBottomR; } } else { this._viewportScrollContainerX = this._viewportScrollContainerY = this._viewportTopR; } } else { this._headerScrollContainer = this._headerScrollerL; this._headerRowScrollContainer = this._headerRowScrollerL; this._footerRowScrollContainer = this._footerRowScrollerL; if (this.hasFrozenRows) { if (this._options.frozenBottom) { this._viewportScrollContainerX = this._viewportBottomL; this._viewportScrollContainerY = this._viewportTopL; } else { this._viewportScrollContainerX = this._viewportScrollContainerY = this._viewportBottomL; } } else { this._viewportScrollContainerX = this._viewportScrollContainerY = this._viewportTopL; } } } protected measureCellPaddingAndBorder(): void { const h = ['borderLeftWidth', 'borderRightWidth', 'paddingLeft', 'paddingRight']; const v = ['borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom']; const header = this._headers[0]; this.headerColumnWidthDiff = this.headerColumnHeightDiff = 0; this.cellWidthDiff = this.cellHeightDiff = 0; let el = createDomElement( 'div', { className: 'slick-state-default slick-header-column', style: { visibility: 'hidden' }, textContent: '-' }, header ); let style = getComputedStyle(el); if (style.boxSizing !== 'border-box') { h.forEach((val) => (this.headerColumnWidthDiff += Utils.toFloat(style[val as any]))); v.forEach((val) => (this.headerColumnHeightDiff += Utils.toFloat(style[val as any]))); } el.remove(); const r = createDomElement('div', { className: 'slick-row' }, this._canvas[0]); el = createDomElement('div', { className: 'slick-cell', id: '', style: { visibility: 'hidden' }, textContent: '-' }, r); style = getComputedStyle(el); if (style.boxSizing !== 'border-box') { h.forEach((val) => (this.cellWidthDiff += Utils.toFloat(style[val as any]))); v.forEach((val) => (this.cellHeightDiff += Utils.toFloat(style[val as any]))); } r.remove(); this.absoluteColumnMinWidth = Math.max(this.headerColumnWidthDiff, this.cellWidthDiff); } protected createCssRules(): void { this._style = document.createElement('style'); this._style.nonce = this._options.nonce || ''; (this._options.shadowRoot || document.head).appendChild(this._style); const rowHeight = this._options.rowHeight! - this.cellHeightDiff; const rules = [ `.${this.uid} .slick-group-header-column { left: 1000px; }`, `.${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; }`, `.${this.uid} .slick-row { height: ${this._options.rowHeight}px; }`, ]; const sheet = this._style.sheet; /* v8 ignore else */ if (sheet) { rules.forEach((rule) => sheet.insertRule(rule)); for (let i = 0; i < this.columns.length; i++) { if (this.columns[i]) { sheet.insertRule(`.${this.uid} .l${i} { }`); sheet.insertRule(`.${this.uid} .r${i} { }`); } } } else { // fallback in case the 1st approach doesn't work, let's use our previous way of creating the css rules which is what works in Salesforce :( this.createCssRulesAlternative(rules); } } /** Create CSS rules via template in case the first approach with createElement('style') doesn't work */ /* v8 ignore next */ protected createCssRulesAlternative(rules: string[]): void { const template = document.createElement('template'); template.innerHTML = '