UNPKG

@serenity-is/sleekgrid

Version:

A modern Data Grid / Spreadsheet component

1,206 lines (1,019 loc) 152 kB
import { CellRange, CellStylesHash, Column, ColumnFormat, ColumnMetadata, ColumnSort, EditCommand, EditController, Editor, EditorClass, EditorHost, EditorLock, EventData, EventEmitter, FormatterContext, FormatterResult, GroupTotals, H, IEventData, ItemMetadata, Position, RowCell, addClass, applyFormatterResultToCellNode, basicRegexSanitizer, columnDefaults, convertCompatFormatter, defaultColumnFormat, disableSelection, escapeHtml, initializeColumns, parsePx, preClickClassName, removeClass } from "../core"; import { BasicLayout } from "./basiclayout"; import { CellNavigator } from "./cellnavigator"; import { Draggable } from "./draggable"; import { ArgsAddNewRow, ArgsCell, ArgsCellChange, ArgsCellEdit, ArgsColumn, ArgsColumnNode, ArgsCssStyle, ArgsEditorDestroy, ArgsGrid, ArgsScroll, ArgsSelectedRowsChange, ArgsSort, ArgsValidationError } from "./eventargs"; import { GridOptions, gridDefaults } from "./gridoptions"; import { CachedRow, PostProcessCleanupEntry, absBox, addUiStateHover, autosizeColumns, calcMinMaxPageXOnDragStart, getInnerWidth, getMaxSupportedCssHeight, getScrollBarDimensions, getVBoxDelta, removeUiStateHover, shrinkOrStretchColumn, simpleArrayEquals, sortToDesiredOrderAndKeepRest } from "./internal"; import { LayoutEngine } from "./layout"; import { IPlugin, SelectionModel, ViewRange, ViewportInfo } from "./types"; export class Grid<TItem = any> implements EditorHost { declare private _absoluteColMinWidth: number; declare private _activeCanvasNode: HTMLElement; declare private _activeCell: number; declare private _activeCellNode: HTMLElement; declare private _activePosX: number; declare private _activeRow: number; declare private _activeViewportNode: HTMLElement; private _cellCssClasses: Record<string, CellStylesHash> = {}; private _cellHeightDiff: number = 0; private _cellWidthDiff: number = 0; declare private _cellNavigator: CellNavigator; declare private _colById: { [key: string]: number }; declare private _colDefaults: Partial<Column>; private _colLeft: number[] = []; private _colRight: number[] = []; declare private _cols: Column<TItem>[]; declare private _columnCssRulesL: any; declare private _columnCssRulesR: any; declare private _currentEditor: Editor; declare private _data: any; declare private _draggableInstance: { destroy: () => void }; declare private _editController: EditController; declare private _emptyNode: (node: Element) => void; private _headerColumnWidthDiff: number = 0; declare private _hEditorLoader: number; declare private _hPostRender: number; declare private _hPostRenderCleanup: number; declare private _hRender: number; private _ignoreScrollUntil: number = 0; declare private _initColById: { [key: string]: number }; declare private _initCols: Column<TItem>[]; declare private _initialized; declare private _jQuery: any; declare private _jumpinessCoefficient: number; declare private _lastRenderTime: number; declare private _layout: LayoutEngine; declare private _numberOfPages: number; declare private _options: GridOptions<TItem>; private _page: number = 0; declare private _pageHeight: number; private _pageOffset: number = 0; private _pagingActive: boolean = false; private _pagingIsLastPage: boolean = false; private _plugins: IPlugin[] = []; declare private _postCleanupActive: boolean; private _postProcessCleanupQueue: PostProcessCleanupEntry[] = []; private _postProcessedRows: { [row: number]: { [cell: number]: string } } = {}; declare private _postProcessFromRow: number; private _postProcessGroupId: number = 0; declare private _postProcessToRow: number; declare private _postRenderActive: boolean; declare private _removeNode: (node: Element) => void; private _rowsCache: { [key: number]: CachedRow } = {}; declare private _scrollDims: { width: number, height: number }; private _scrollLeft: number = 0; private _scrollLeftPrev: number = 0; private _scrollLeftRendered: number = 0; private _scrollTop: number = 0; private _scrollTopPrev: number = 0; private _scrollTopRendered: number = 0; private _selectedRows: number[] = []; declare private _selectionModel: SelectionModel; declare private _serializedEditorValue: any; private _sortColumns: ColumnSort[] = []; declare private _styleNode: HTMLStyleElement; declare private _stylesheet: any; private _tabbingDirection: number = 1; private _uid: string = "sleekgrid_" + Math.round(1000000 * Math.random()); private _viewportInfo: ViewportInfo = {} as any; private _vScrollDir: number = 1; private _boundAncestorScroll: HTMLElement[] = []; declare private _container: HTMLElement; declare private _focusSink1: HTMLElement; declare private _focusSink2: HTMLElement; declare private _groupingPanel: HTMLElement; readonly onActiveCellChanged = new EventEmitter<ArgsCell>(); readonly onActiveCellPositionChanged = new EventEmitter<ArgsGrid>(); readonly onAddNewRow = new EventEmitter<ArgsAddNewRow>(); readonly onBeforeCellEditorDestroy = new EventEmitter<ArgsEditorDestroy>(); readonly onBeforeDestroy = new EventEmitter<ArgsGrid>(); readonly onBeforeEditCell = new EventEmitter<ArgsCellEdit>(); readonly onBeforeFooterRowCellDestroy = new EventEmitter<ArgsColumnNode>(); readonly onBeforeHeaderCellDestroy = new EventEmitter<ArgsColumnNode>(); readonly onBeforeHeaderRowCellDestroy = new EventEmitter<ArgsColumnNode>(); readonly onCellChange = new EventEmitter<ArgsCellChange>(); readonly onCellCssStylesChanged = new EventEmitter<ArgsCssStyle>(); readonly onClick = new EventEmitter<ArgsCell, MouseEvent>(); readonly onColumnsReordered = new EventEmitter<ArgsGrid>(); readonly onColumnsResized = new EventEmitter<ArgsGrid>(); readonly onCompositeEditorChange = new EventEmitter<ArgsGrid>(); readonly onContextMenu = new EventEmitter<ArgsGrid, UIEvent>(); readonly onDblClick = new EventEmitter<ArgsCell, MouseEvent>(); readonly onDrag = new EventEmitter<ArgsGrid, UIEvent>(); readonly onDragEnd = new EventEmitter<ArgsGrid, UIEvent>(); readonly onDragInit = new EventEmitter<ArgsGrid, UIEvent>(); readonly onDragStart = new EventEmitter<ArgsGrid, UIEvent>(); readonly onFooterRowCellRendered = new EventEmitter<ArgsColumnNode>(); readonly onHeaderCellRendered = new EventEmitter<ArgsColumnNode>(); readonly onHeaderClick = new EventEmitter<ArgsColumn>(); readonly onHeaderContextMenu = new EventEmitter<ArgsColumn>(); readonly onHeaderMouseEnter = new EventEmitter<ArgsColumn, MouseEvent>(); readonly onHeaderMouseLeave = new EventEmitter<ArgsColumn, MouseEvent>(); readonly onHeaderRowCellRendered = new EventEmitter<ArgsColumnNode>(); readonly onKeyDown = new EventEmitter<ArgsCell, KeyboardEvent>(); readonly onMouseEnter = new EventEmitter<ArgsGrid, MouseEvent>(); readonly onMouseLeave = new EventEmitter<ArgsGrid, MouseEvent>(); readonly onScroll = new EventEmitter<ArgsScroll>(); readonly onSelectedRowsChanged = new EventEmitter<ArgsSelectedRowsChange>(); readonly onSort = new EventEmitter<ArgsSort>(); readonly onValidationError = new EventEmitter<ArgsValidationError>(); readonly onViewportChanged = new EventEmitter<ArgsGrid>(); constructor(container: string | HTMLElement | ArrayLike<HTMLElement>, data: any, columns: Column<TItem>[], options: GridOptions<TItem>) { this._data = data; this._colDefaults = Object.assign({}, columnDefaults); this._options = options = Object.assign({}, gridDefaults, options); // @ts-ignore options.jQuery = this._jQuery = options.jQuery === void 0 ? (typeof jQuery !== "undefined" ? jQuery : void 0) : options.jQuery; // @ts-ignore options.sanitizer = options.sanitizer === void 0 ? (typeof DOMPurify !== "undefined" && typeof DOMPurify.sanitize == "function" ? DOMPurify.sanitize : basicRegexSanitizer) : options.sanitizer; if (this._jQuery && container instanceof (this._jQuery as any)) this._container = (container as any)[0]; else if (container instanceof Element) this._container = container as HTMLElement; else if (typeof container === "string") this._container = document.querySelector(container); else if (container.length) container = container[0]; if (this._container == null) { throw new Error("SleekGrid requires a valid container, " + container + " does not exist in the DOM."); } this._container.classList.add('slick-container'); this._emptyNode = options.emptyNode ?? (this._jQuery ? (function (node: Element) { this(node).empty(); }).bind(this._jQuery) : (function (node: Element) { node.innerHTML = ""; })); this._removeNode = options.removeNode ?? (this._jQuery ? (function (node: Element) { this(node).remove(); }).bind(this._jQuery) : (function (node: Element) { node.remove(); })); if (options?.createPreHeaderPanel) { // for compat, as draggable grouping plugin expects preHeaderPanel for grouping if (options.groupingPanel == null) options.groupingPanel = true; if (options.groupingPanelHeight == null && options.preHeaderPanelHeight != null) options.groupingPanelHeight = options.preHeaderPanelHeight; if (options.showGroupingPanel == null && options.showPreHeaderPanel != null) options.showGroupingPanel = options.showPreHeaderPanel; } this._options.rtl = this._options.rtl ?? (document.body.classList.contains('rtl') || (typeof getComputedStyle != "undefined" && getComputedStyle(this._container).direction == 'rtl')); if (this._options.rtl) this._container.classList.add('rtl'); else this._container.classList.add('ltr'); this.validateAndEnforceOptions(); this._colDefaults.width = options.defaultColumnWidth; this._editController = { "commitCurrentEdit": this.commitCurrentEdit.bind(this), "cancelCurrentEdit": this.cancelCurrentEdit.bind(this) }; if (this._jQuery) this._jQuery(this._container).empty(); else this._container.innerHTML = ''; this._container.style.overflow = "hidden"; this._container.style.outline = "0"; this._container.classList.add(this._uid); if (this._options.useLegacyUI) this._container.classList.add("ui-widget"); // set up a positioning container if needed if (!/relative|absolute|fixed/.test(getComputedStyle(this._container).position)) { this._container.style.position = "relative"; } this._container.appendChild(this._focusSink1 = H('div', { class: "slick-focus-sink", hideFocus: '', style: 'position:fixed;width:0!important;height:0!important;top:0;left:0;outline:0!important;', tabIndex: '0' })); this._layout = options.layoutEngine ?? new BasicLayout(); this.setInitialCols(columns); this._scrollDims = getScrollBarDimensions(); if (options.groupingPanel) { this.createGroupingPanel(); } this._layout.init({ cleanUpAndRenderCells: this.cleanUpAndRenderCells.bind(this), bindAncestorScroll: this.bindAncestorScroll.bind(this), getAvailableWidth: this.getAvailableWidth.bind(this), getCellFromPoint: this.getCellFromPoint.bind(this), getColumnCssRules: this.getColumnCssRules.bind(this), getColumns: this.getColumns.bind(this), getContainerNode: this.getContainerNode.bind(this), getDataLength: this.getDataLength.bind(this), getOptions: this.getOptions.bind(this), getRowFromNode: this.getRowFromNode.bind(this), getScrollDims: this.getScrollBarDimensions.bind(this), getScrollLeft: () => this._scrollLeft, getScrollTop: () => this._scrollTop, getViewportInfo: () => this._viewportInfo, renderRows: this.renderRows.bind(this) }); this._container.append(this._focusSink2 = this._focusSink1.cloneNode() as HTMLElement); if (options.viewportClass) this.getViewports().forEach(vp => addClass(vp, options.viewportClass)); if (!options.explicitInitialization) { this.init(); } this.bindToData(); } private createGroupingPanel() { if (this._groupingPanel || !this._focusSink1) return; this._focusSink1.insertAdjacentElement("afterend", this._groupingPanel = H('div', { class: "slick-grouping-panel", style: (!this._options.showGroupingPanel ? "display: none" : null) })); if (this._options.createPreHeaderPanel) { this._groupingPanel.appendChild(H('div', { class: 'slick-preheader-panel' })); } } private bindAncestorScroll(elem: HTMLElement) { if (this._jQuery) this._jQuery(elem).on('scroll', this.handleActiveCellPositionChange); else elem.addEventListener('scroll', this.handleActiveCellPositionChange); this._boundAncestorScroll.push(elem); } init(): void { if (this._initialized) return; this._initialized = true; this.calcViewportSize(); // 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(); var viewports = this.getViewports(); if (this._jQuery && !this._options.enableTextSelectionOnCells) { // disable text selection in grid cells except in input and textarea elements // (this is IE-specific, because selectstart event will only fire in IE) this._jQuery(viewports).on("selectstart.ui", () => { return this._jQuery(this).is("input,textarea"); }); } this._layout.setPaneVisibility(); this._layout.setScroller(); this.setOverflow(); this.updateViewColLeftRight(); this.createColumnHeaders(); this.createColumnFooters(); this.setupColumnSort(); this.createCssRules(); this.resizeCanvas(); this._layout.bindAncestorScrollEvents(); const onEvent = <K extends keyof HTMLElementEventMap>(el: HTMLElement, type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any) => { if (this._jQuery) this._jQuery(el).on(type, listener as any); else el.addEventListener(type, listener); } onEvent(this._container, "resize", this.resizeCanvas); viewports.forEach(vp => { var scrollTicking = false; onEvent(vp, "scroll", (e) => { if (!scrollTicking) { scrollTicking = true; window.requestAnimationFrame(() => { this.handleScroll(); scrollTicking = false; }); } }); }); if (this._jQuery && (this._jQuery.fn as any).mousewheel && (this.hasFrozenColumns() || this.hasFrozenRows())) { this._jQuery(viewports).on("mousewheel", this.handleMouseWheel.bind(this)); } this._layout.getHeaderCols().forEach(hs => { disableSelection(hs); onEvent(hs, "contextmenu", this.handleHeaderContextMenu.bind(this)); onEvent(hs, "click", this.handleHeaderClick.bind(this)); if (this._jQuery) { this._jQuery(hs) .on('mouseenter', '.slick-header-column', this.handleHeaderMouseEnter.bind(this)) .on('mouseleave', '.slick-header-column', this.handleHeaderMouseLeave.bind(this)); } else { // need to reimplement this similar to jquery events hs.addEventListener("mouseenter", e => (e.target as HTMLElement).closest(".slick-header-column") && this.handleHeaderMouseEnter(e)); hs.addEventListener("mouseleave", e => (e.target as HTMLElement).closest(".slick-header-column") && this.handleHeaderMouseLeave(e)); } }); this._layout.getHeaderRowCols().forEach(el => { onEvent(el.parentElement, 'scroll', this.handleHeaderRowScroll); }); this._layout.getFooterRowCols().forEach(el => { onEvent(el.parentElement, 'scroll', this.handleFooterRowScroll); }); [this._focusSink1, this._focusSink2].forEach(fs => onEvent(fs, "keydown", this.handleKeyDown.bind(this))); var canvases = Array.from<HTMLElement>(this.getCanvases()); canvases.forEach(canvas => { onEvent(canvas, "keydown", this.handleKeyDown.bind(this)) onEvent(canvas, "click", this.handleClick.bind(this)) onEvent(canvas, "dblclick", this.handleDblClick.bind(this)) onEvent(canvas, "contextmenu", this.handleContextMenu.bind(this)); }); if (this._jQuery && (this._jQuery.fn as any).drag) { this._jQuery(canvases) .on("draginit", this.handleDragInit.bind(this)) .on("dragstart", { distance: 3 }, this.handleDragStart.bind(this)) .on("drag", this.handleDrag.bind(this)) .on("dragend", this.handleDragEnd.bind(this)) } else { this._draggableInstance = Draggable({ containerElement: this._container, //allowDragFrom: 'div.slick-cell', // the slick cell parent must always contain `.dnd` and/or `.cell-reorder` class to be identified as draggable //allowDragFromClosest: 'div.slick-cell.dnd, div.slick-cell.cell-reorder', preventDragFromKeys: ['ctrlKey', 'metaKey'], onDragInit: this.handleDragInit.bind(this), onDragStart: this.handleDragStart.bind(this), onDrag: this.handleDrag.bind(this), onDragEnd: this.handleDragEnd.bind(this) }); } canvases.forEach(canvas => { if (this._jQuery) { this._jQuery(canvas) .on('mouseenter', '.slick-cell', this.handleMouseEnter.bind(this)) .on('mouseleave', '.slick-cell', this.handleMouseLeave.bind(this)); } else { canvas.addEventListener("mouseenter", e => (e.target as HTMLElement)?.classList?.contains("slick-cell") && this.handleMouseEnter(e), { capture: true }); canvas.addEventListener("mouseleave", e => (e.target as HTMLElement)?.classList?.contains("slick-cell") && this.handleMouseLeave(e), { capture: true }); } }); // Work around http://crbug.com/312427. if (navigator.userAgent.toLowerCase().match(/webkit/) && navigator.userAgent.toLowerCase().match(/macintosh/) && this._jQuery) { this._jQuery(canvases).on("mousewheel", this.handleMouseWheel.bind(this)); } } private hasFrozenColumns(): boolean { return this._layout.getFrozenCols() > 0; } private hasFrozenRows(): boolean { return this._layout.getFrozenRows() > 0; } registerPlugin(plugin: IPlugin): void { this._plugins.unshift(plugin); plugin.init(this); } unregisterPlugin(plugin: IPlugin): void { for (var i = this._plugins.length; i >= 0; i--) { if (this._plugins[i] === plugin) { if (this._plugins[i].destroy) { this._plugins[i].destroy(); } this._plugins.splice(i, 1); break; } } } getPluginByName(name: string): IPlugin { for (var i = this._plugins.length - 1; i >= 0; i--) { if (this._plugins[i].pluginName === name) return this._plugins[i]; } } setSelectionModel(model: SelectionModel): void { this.unregisterSelectionModel(); this._selectionModel = model; if (this._selectionModel) { this._selectionModel.init(this); this._selectionModel.onSelectedRangesChanged.subscribe(this.handleSelectedRangesChanged); } } private unregisterSelectionModel(): void { if (!this._selectionModel) return; this._selectionModel.onSelectedRangesChanged.unsubscribe(this.handleSelectedRangesChanged); this._selectionModel.destroy?.(); } getScrollBarDimensions(): { width: number; height: number; } { return this._scrollDims; } getDisplayedScrollbarDimensions(): { width: number; height: number; } { return { width: this._viewportInfo.hasVScroll ? this._scrollDims.width : 0, height: this._viewportInfo.hasHScroll ? this._scrollDims.height : 0 }; } getAbsoluteColumnMinWidth() { return this._absoluteColMinWidth; } getSelectionModel(): SelectionModel { return this._selectionModel; } private colIdOrIdxToCell(columnIdOrIdx: string | number): number { if (columnIdOrIdx == null) return null; if (typeof columnIdOrIdx !== "number") return this.getColumnIndex(columnIdOrIdx); return columnIdOrIdx; } getCanvasNode(columnIdOrIdx?: string | number, row?: number): HTMLElement { return this._layout.getCanvasNodeFor(this.colIdOrIdxToCell(columnIdOrIdx || 0), row || 0); } getCanvases(): any | HTMLElement[] { var canvases = this._layout.getCanvasNodes(); return this._jQuery ? this._jQuery(canvases) : canvases; } getActiveCanvasNode(e?: IEventData): HTMLElement { if (e) { // compatibility with celldecorator plugin this._activeCanvasNode = (e.target as HTMLElement).closest('.grid-canvas'); } return this._activeCanvasNode; } getViewportNode(columnIdOrIdx?: string | number, row?: number): HTMLElement { return this._layout.getViewportNodeFor(this.colIdOrIdxToCell(columnIdOrIdx || 0), row || 0); } private getViewports(): HTMLElement[] { return this._layout.getViewportNodes(); } getActiveViewportNode(e?: IEventData): HTMLElement { if (e) { // compatibility with celldecorator plugin this._activeViewportNode = (e.target as HTMLElement).closest('.slick-viewport'); } return this._activeViewportNode; } private getAvailableWidth() { return this._viewportInfo.hasVScroll ? this._viewportInfo.width - this._scrollDims.width : this._viewportInfo.width; } private updateCanvasWidth(forceColumnWidthsUpdate?: boolean): void { const widthChanged = this._layout.updateCanvasWidth(); if (widthChanged || forceColumnWidthsUpdate) { this._layout.applyColumnWidths(); } } private unbindAncestorScrollEvents(): void { if (this._boundAncestorScroll) { for (var x of this._boundAncestorScroll) x.removeEventListener('scroll', this.handleActiveCellPositionChange); } this._boundAncestorScroll = []; } updateColumnHeader(columnId: string, title?: string, toolTip?: string): void { if (!this._initialized) { return; } var idx = this.getColumnIndex(columnId); if (idx == null) { return; } var columnDef = this._cols[idx]; var header = this._layout.getHeaderColumn(idx); if (!header) return; if (title !== undefined) { columnDef.name = title; } if (toolTip !== undefined) { columnDef.toolTip = toolTip; } this.trigger(this.onBeforeHeaderCellDestroy, { node: header, column: columnDef }); if (toolTip !== undefined) header.title = toolTip || ""; if (title !== undefined) { var child = header.firstElementChild; if (columnDef.nameIsHtml) child && (child.innerHTML = title ?? ''); else child && (child.textContent = title ?? '') } this.trigger(this.onHeaderCellRendered, { node: header, column: columnDef }); } getHeader(): HTMLElement { return this._layout.getHeaderCols()[0]; } getHeaderColumn(columnIdOrIdx: string | number): HTMLElement { var cell = this.colIdOrIdxToCell(columnIdOrIdx); if (cell == null) return null; return this._layout.getHeaderColumn(cell); } getGroupingPanel(): HTMLElement { return this._groupingPanel; } getPreHeaderPanel(): HTMLElement { return this._groupingPanel?.querySelector('.slick-preheader-panel'); } getHeaderRow(): HTMLElement { return this._layout.getHeaderRowCols()[0]; } getHeaderRowColumn(columnIdOrIdx: string | number): HTMLElement { var cell = this.colIdOrIdxToCell(columnIdOrIdx); if (cell == null) return; return this._layout.getHeaderRowColumn(cell); } getFooterRow(): HTMLElement { return this._layout.getFooterRowCols()[0]; } getFooterRowColumn(columnIdOrIdx: string | number): HTMLElement { var cell = this.colIdOrIdxToCell(columnIdOrIdx); if (cell == null) return null; return this._layout.getFooterRowColumn(cell); } private createColumnFooters(): void { var footerRowCols = this._layout.getFooterRowCols(); footerRowCols.forEach(frc => { frc.querySelectorAll(".slick-footerrow-column") .forEach((el) => { var columnDef = this.getColumnFromNode(el); if (columnDef) { this.trigger(this.onBeforeFooterRowCellDestroy, { node: el as HTMLElement, column: columnDef }); } }) if (this._jQuery) { this._jQuery(frc).empty(); } else frc.innerHTML = ''; }); var cols = this._cols; for (var i = 0; i < cols.length; i++) { var m = cols[i]; var footerRowCell = H("div", { class: "slick-footerrow-column l" + i + " r" + i + (this._options.useLegacyUI ? ' ui-state-default' : '') }); footerRowCell.dataset.c = i.toString(); this._jQuery && this._jQuery(footerRowCell).data("column", m); if (m.footerCssClass) addClass(footerRowCell, m.footerCssClass); else if (m.cssClass) addClass(footerRowCell, m.cssClass); this._layout.getFooterRowColsFor(i).appendChild(footerRowCell); this.trigger(this.onFooterRowCellRendered, { node: footerRowCell, column: m }); } } private createColumnHeaders(): void { const headerCols = this._layout.getHeaderCols(); headerCols.forEach(hc => { hc.querySelectorAll(".slick-header-column") .forEach((el) => { var columnDef = this.getColumnFromNode(el); if (columnDef) { this.trigger(this.onBeforeHeaderCellDestroy, { node: el as HTMLElement, column: columnDef }); } }); this._emptyNode(hc); }); this._layout.updateHeadersWidth(); const headerRowCols = this._layout.getHeaderRowCols(); headerRowCols.forEach(hrc => { hrc.querySelectorAll(".slick-headerrow-column") .forEach((el) => { var columnDef = this.getColumnFromNode(el); if (columnDef) { this.trigger(this.onBeforeHeaderRowCellDestroy, { node: el as HTMLElement, column: columnDef, grid: this }); } }); if (this._jQuery) { this._jQuery(hrc).empty(); } else { hrc.innerHTML = ""; } }); var cols = this._cols, frozenCols = this._layout.getFrozenCols(); for (var i = 0; i < cols.length; i++) { var m = cols[i]; var headerTarget = this._layout.getHeaderColsFor(i); var name = document.createElement("span"); name.className = "slick-column-name"; if (m.nameIsHtml) name.innerHTML = m.name ?? ''; else name.textContent = (m.name ?? ''); var header = H("div", { class: "slick-header-column" + (this._options.useLegacyUI ? " ui-state-default " : ""), ["data-id"]: m.id, id: "" + this._uid + m.id, title: m.toolTip || "", style: "width: " + (m.width - this._headerColumnWidthDiff) + "px" }, name); header.dataset.c = i.toString(); this._jQuery && this._jQuery(header).data("column", m); m.headerCssClass && addClass(header, m.headerCssClass); (i < frozenCols) && header.classList.add("frozen"); headerTarget.appendChild(header); if ((this._options.enableColumnReorder || m.sortable) && this._options.useLegacyUI) { if (this._jQuery) { this._jQuery(header).on("mouseenter", addUiStateHover); this._jQuery(header).on("mouseleave", removeUiStateHover); } else { header.addEventListener('mouseenter', addUiStateHover); header.addEventListener('mouseleave', removeUiStateHover); } } if (m.sortable) { header.classList.add("slick-header-sortable"); header.appendChild(H("span", { class: "slick-sort-indicator" })); } this.trigger(this.onHeaderCellRendered, { node: header, column: m }); if (this._options.showHeaderRow) { var headerRowTarget = this._layout.getHeaderRowColsFor(i); var headerRowCell = H("div", { class: "slick-headerrow-column l" + i + " r" + i + (this._options.useLegacyUI ? " ui-state-default" : "") }); headerRowCell.dataset.c = i.toString(); this._jQuery && this._jQuery(headerRowCell).data("column", m); headerRowTarget.appendChild(headerRowCell); this.trigger(this.onHeaderRowCellRendered, { node: headerRowCell, column: m }); } } this.setSortColumns(this._sortColumns); this.setupColumnResize(); if (this._options.enableColumnReorder) { this.setupColumnReorder(); // sortable js removes draggable attribute after disposing / recreating this._layout.getHeaderCols().forEach(el => el.querySelectorAll<HTMLDivElement>(".slick-resizable-handle").forEach(x => x.draggable = true)); } } private setupColumnSort(): void { const handler = (e: MouseEvent) => { var tgt = e.target as Element; if (tgt.classList.contains("slick-resizable-handle")) { return; } var colNode = tgt.closest(".slick-header-column"); if (!colNode) { return; } var column = this.getColumnFromNode(colNode); if (column.sortable) { if (!this.getEditorLock().commitCurrentEdit()) { return; } var sortOpts = null; var i = 0; for (; i < this._sortColumns.length; i++) { if (this._sortColumns[i].columnId == column.id) { sortOpts = this._sortColumns[i]; sortOpts.sortAsc = !sortOpts.sortAsc; break; } } if (e.metaKey && this._options.multiColumnSort) { if (sortOpts) { this._sortColumns.splice(i, 1); } } else { if ((!e.shiftKey && !e.metaKey) || !this._options.multiColumnSort) { this._sortColumns = []; } if (!sortOpts) { sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc }; this._sortColumns.push(sortOpts); } else if (this._sortColumns.length == 0) { this._sortColumns.push(sortOpts); } } this.setSortColumns(this._sortColumns); if (!this._options.multiColumnSort) { this.trigger(this.onSort, { multiColumnSort: false, sortCol: column, sortAsc: sortOpts.sortAsc }, e); } else { var cols = this._initCols; this.trigger(this.onSort, { multiColumnSort: true, sortCols: this._sortColumns.map(col => ({ sortCol: cols[this.getInitialColumnIndex(col.columnId)], sortAsc: col.sortAsc })) }, e); } } }; this._layout.getHeaderCols().forEach(el => { if (this._jQuery) this._jQuery(el).on('click', handler as any); else el.addEventListener("click", handler); }); } private static offset(el: HTMLElement | null) { if (!el || !el.getBoundingClientRect) return; const box = el.getBoundingClientRect(); const docElem = document.documentElement; return { top: box.top + window.scrollY - docElem.clientTop, left: box.left + window.scrollX - docElem.clientLeft }; } declare private sortableColInstances: any[]; private setupColumnReorder(): void { // @ts-ignore if (typeof Sortable === "undefined") return; this.sortableColInstances?.forEach(x => x.destroy()); let columnScrollTimer: number = null; const scrollColumnsLeft = () => this._layout.getScrollContainerX().scrollLeft = this._layout.getScrollContainerX().scrollLeft + 10; const scrollColumnsRight = () => this._layout.getScrollContainerX().scrollLeft = this._layout.getScrollContainerX().scrollLeft - 10; let canDragScroll; const sortableOptions: any = { animation: 50, direction: 'horizontal', chosenClass: 'slick-header-column-active', ghostClass: 'slick-sortable-placeholder', draggable: '.slick-header-column', filter: ".slick-resizable-handle", preventOnFilter: false, dragoverBubble: false, revertClone: true, scroll: !this.hasFrozenColumns(), // enable auto-scroll onStart: (e: { item: any; originalEvent: MouseEvent; }) => { canDragScroll = !this.hasFrozenColumns() || Grid.offset(e.item)!.left > Grid.offset(this._layout.getScrollContainerX())!.left; if (canDragScroll && e.originalEvent && e.originalEvent.pageX > this._container.clientWidth) { if (!(columnScrollTimer)) { columnScrollTimer = setInterval(scrollColumnsRight, 100); } } else if (canDragScroll && e.originalEvent && e.originalEvent.pageX < Grid.offset(this._layout.getScrollContainerX())!.left) { if (!(columnScrollTimer)) { columnScrollTimer = setInterval(scrollColumnsLeft, 100); } } else { clearInterval(columnScrollTimer); columnScrollTimer = null; } }, onEnd: (e: MouseEvent & { item: any; originalEvent: MouseEvent; }) => { const cancel = false; clearInterval(columnScrollTimer); columnScrollTimer = null; if (cancel || !this.getEditorLock()?.commitCurrentEdit()) { return; } var reorderedCols; this._layout.getHeaderCols().forEach((el, i) => reorderedCols = sortToDesiredOrderAndKeepRest( this._initCols, (this.sortableColInstances[i]?.toArray?.() ?? []) )); this.setColumns(reorderedCols); this.trigger(this.onColumnsReordered, {}); e.stopPropagation(); this.setupColumnResize(); if (this._activeCellNode) { this.setFocus(); // refocus on active cell } } } // @ts-ignore this.sortableColInstances = this._layout.getHeaderCols().map(x => Sortable.create(x, sortableOptions)); } private setupColumnResize(): void { var minPageX: number, pageX: number, maxPageX: number, cols = this._cols; var columnElements: Element[] = []; this._layout.getHeaderCols().forEach(el => { columnElements = columnElements.concat(Array.from(el.children)); }); var j: number, c: Column<TItem>, pageX: number, minPageX: number, maxPageX: number, firstResizable: number, lastResizable: number, cols = this._cols; var firstResizable: number, lastResizable: number; columnElements.forEach((el, i) => { var handle = el.querySelector(".slick-resizable-handle"); handle && this._removeNode(handle); if (cols[i].resizable) { if (firstResizable === undefined) { firstResizable = i; } lastResizable = i; } }); if (firstResizable === undefined) { return; } const noJQueryDrag = !this._jQuery || !this._jQuery.fn || !(this._jQuery.fn as any).drag; columnElements.forEach((el, colIdx) => { if (colIdx < firstResizable || (this._options.forceFitColumns && colIdx >= lastResizable)) { return; } const handle = el.appendChild(document.createElement('div')); handle.classList.add('slick-resizable-handle'); handle.draggable = true; var docDragOver: any = null; var lastDragOverPos: any = null; const dragStart = (e: DragEvent) => { if (!this.getEditorLock().commitCurrentEdit()) { !noJQueryDrag && e.preventDefault(); return; } if (noJQueryDrag) { docDragOver = (z: DragEvent) => { lastDragOverPos = { pageX: z.pageX, pageY: z.pageY }; z.preventDefault(); } document.addEventListener('dragover', docDragOver); } pageX = e.pageX; (e.target as HTMLElement).parentElement?.classList.add("slick-header-column-active"); // lock each column's width option to current width columnElements.forEach((e, z) => { cols[z].previousWidth = (e as HTMLElement).offsetWidth; }); const minMax = calcMinMaxPageXOnDragStart(cols, colIdx, pageX, this._options.forceFitColumns, this._absoluteColMinWidth); maxPageX = minMax.maxPageX; minPageX = minMax.minPageX; noJQueryDrag && (e.dataTransfer.effectAllowed = 'move'); }; const drag = (e: DragEvent) => { var dist; if (noJQueryDrag) { var thisPageX = (!e.pageX && !e.pageY) ? lastDragOverPos?.pageX : e.pageX; var thisPageY = (!e.pageX && !e.pageY) ? lastDragOverPos?.pageY : e.pageY; if (!thisPageX && !e.clientX && !thisPageY && !e.clientY) return; dist = Math.min(maxPageX, Math.max(minPageX, thisPageX)) - pageX; e.dataTransfer.effectAllowed = 'none'; e.preventDefault(); } else { dist = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX; } if (isNaN(dist)) { return; } shrinkOrStretchColumn(cols, colIdx, dist, this._options.forceFitColumns, this._absoluteColMinWidth); this._layout.afterHeaderColumnDrag(); this.applyColumnHeaderWidths(); if (this._options.syncColumnCellResize) { this._layout.applyColumnWidths(); } } const dragEnd = (e: any) => { if (docDragOver) { document.removeEventListener('dragover', docDragOver); docDragOver = null; } (e.target.parentElement as HTMLElement)?.classList.remove("slick-header-column-active"); for (j = 0; j < columnElements.length; j++) { c = cols[j]; var newWidth = (columnElements[j] as HTMLElement).offsetWidth; if (c.previousWidth !== newWidth && c.rerenderOnResize) { this.invalidateAllRows(); } } this.columnsResized(false); } if (noJQueryDrag) { handle.addEventListener("dragstart", dragStart); handle.addEventListener("drag", drag); handle.addEventListener("dragend", dragEnd); handle.addEventListener("dragover", (e: any) => { e.preventDefault(); e.dataTransfer.effectAllowed = "move"; }); } else { (this._jQuery(handle) as any) .on("dragstart", dragStart) .on("drag", drag) .on("dragend", dragEnd); } }); } public columnsResized(invalidate = true) { this.applyColumnHeaderWidths(); this._layout.applyColumnWidths(); invalidate && this.invalidateAllRows(); this.updateCanvasWidth(true); this.render(); this.trigger(this.onColumnsResized); } private setOverflow(): void { this._layout.setOverflow(); if (this._options.viewportClass) this.getViewports().forEach(vp => addClass(vp, this._options.viewportClass)); } private measureCellPaddingAndBorder(): void { const h = ["border-left-width", "border-right-width", "padding-left", "padding-right"]; const v = ["border-top-width", "border-bottom-width", "padding-top", "padding-bottom"]; var el = this._layout.getHeaderColsFor(0).appendChild(H("div", { class: "slick-header-column" + (this._options.useLegacyUI ? " ui-state-default" : ""), style: "visibility:hidden" })); this._headerColumnWidthDiff = 0; var cs = getComputedStyle(el); if (cs.boxSizing != "border-box") h.forEach(val => this._headerColumnWidthDiff += parsePx(cs.getPropertyValue(val)) || 0); el.remove(); var r = this._layout.getCanvasNodeFor(0, 0).appendChild(H("div", { class: "slick-row" }, el = H("div", { class: "slick-cell", id: "", style: "visibility: hidden" }))); el.innerHTML = "-"; this._cellWidthDiff = this._cellHeightDiff = 0; cs = getComputedStyle(el); if (cs.boxSizing != "border-box") { h.forEach(val => this._cellWidthDiff += parsePx(cs.getPropertyValue(val)) || 0); v.forEach(val => this._cellHeightDiff += parsePx(cs.getPropertyValue(val)) || 0); } r.remove(); this._absoluteColMinWidth = Math.max(this._headerColumnWidthDiff, this._cellWidthDiff); } private createCssRules() { var cellHeight = (this._options.rowHeight - this._cellHeightDiff); if (this._options.useCssVars && this.getColumns().length > 50) this._options.useCssVars = false; this._container.classList.toggle('sleek-vars', !!this._options.useCssVars); if (this._options.useCssVars) { var style = this._container.style; style.setProperty("--sleek-row-height", this._options.rowHeight + "px"); style.setProperty("--sleek-cell-height", cellHeight + "px"); style.setProperty("--sleek-top-panel-height", this._options.topPanelHeight + "px"); style.setProperty("--sleek-grouping-panel-height", this._options.groupingPanelHeight + "px"); style.setProperty("--sleek-headerrow-height", this._options.headerRowHeight + "px"); style.setProperty("--sleek-footerrow-height", this._options.footerRowHeight + "px"); return; } var el = this._styleNode = document.createElement('style'); el.dataset.uid = this._uid; var rules = [ "." + this._uid + " { --slick-cell-height: " + this._options.rowHeight + "px; }", "." + this._uid + " .slick-group-header-column { " + (this._options.rtl ? 'right' : 'left') + ": 1000px; }", "." + this._uid + " .slick-header-column { " + (this._options.rtl ? 'right' : 'left') + ": 1000px; }", "." + this._uid + " .slick-top-panel { height:" + this._options.topPanelHeight + "px; }", "." + this._uid + " .slick-grouping-panel { height:" + this._options.groupingPanelHeight + "px; }", "." + this._uid + " .slick-headerrow-columns { height:" + this._options.headerRowHeight + "px; }", "." + this._uid + " .slick-cell { height:" + cellHeight + "px; }", "." + this._uid + " .slick-row { height:" + this._options.rowHeight + "px; }", "." + this._uid + " .slick-footerrow-columns { height:" + this._options.footerRowHeight + "px; }", ]; var cols = this._cols; for (var i = 0; i < cols.length; i++) { rules.push("." + this._uid + " .l" + i + " { }"); rules.push("." + this._uid + " .r" + i + " { }"); } el.appendChild(document.createTextNode(rules.join(" "))); document.head.appendChild(el); } private getColumnCssRules(idx: number): { right: any; left: any; } { if (this._options.useCssVars) return null; if (!this._stylesheet) { var stylesheetFromUid = document.querySelector("style[data-uid='" + this._uid + "']") as any if (stylesheetFromUid && stylesheetFromUid.sheet) { this._stylesheet = stylesheetFromUid.sheet; } else { var sheets = document.styleSheets; for (var i = 0; i < sheets.length; i++) { if ((sheets[i].ownerNode || (sheets[i] as any).owningElement) == this._styleNode) { this._stylesheet = sheets[i]; break; } } } if (!this._stylesheet) { throw new Error("Cannot find stylesheet."); } // find and cache column CSS rules this._columnCssRulesL = []; this._columnCssRulesR = []; var cssRules = (this._stylesheet.cssRules || this._stylesheet.rules); var matches, columnIdx; for (var i = 0; i < cssRules.length; i++) { var selector = cssRules[i].selectorText; if (matches = /\.l\d+/.exec(selector)) { columnIdx = parseInt(matches[0].substring(2, matches[0].length), 10); this._columnCssRulesL[columnIdx] = cssRules[i]; } else if (matches = /\.r\d+/.exec(selector)) { columnIdx = parseInt(matches[0].substring(2, matches[0].length), 10); this._columnCssRulesR[columnIdx] = cssRules[i]; } } } return this._options.rtl ? { "right": this._columnCssRulesL[idx], "left": this._columnCssRulesR[idx] } : { "left": this._columnCssRulesL[idx], "right": this._columnCssRulesR[idx] } } private removeCssRules() { this._styleNode?.remove(); this._styleNode = null; this._stylesheet = null; } destroy() { this.getEditorLock().cancelCurrentEdit(); this.trigger(this.onBeforeDestroy); var i = this._plugins.length; while (i--) {