@serenity-is/sleekgrid
Version:
A modern Data Grid / Spreadsheet component
1,206 lines (1,019 loc) • 152 kB
text/typescript
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--) {