From 78bc3fddefc0135a71a2456854c442de7b662a45 Mon Sep 17 00:00:00 2001 From: PB <37089506+pbower@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:04:21 +0530 Subject: [PATCH] refactor: Refactored slick.grid.ts module into function groups. (#1106) * refactor: Refactored slick.grid.ts module into function groups. --- src/slick.grid.ts | 7542 ++++++++++++++++++++++++++------------------- 1 file changed, 4358 insertions(+), 3184 deletions(-) diff --git a/src/slick.grid.ts b/src/slick.grid.ts index 0a517dbd..596bc601 100644 --- a/src/slick.grid.ts +++ b/src/slick.grid.ts @@ -607,8 +607,9 @@ export class SlickGrid = Column, O e this.initialize(options); } - ////////////////////////////////////////////////////////////////////////////////////////////// - // Initialization +////////////////////////////////////////////////////////////////////////////////////////////// +// Grid and Dom Initialisation +////////////////////////////////////////////////////////////////////////////////////////////// /** Initializes the grid. */ init() { @@ -616,49 +617,13 @@ export class SlickGrid = Column, O e } /** - * Apply HTML code by 3 different ways depending on what is provided as input and what options are enabled. - * 1. value is an HTMLElement or DocumentFragment, then first empty the target and simply append the HTML to the target element. - * 2. value is string and `enableHtmlRendering` is enabled, then use `target.innerHTML = value;` - * 3. value is string and `enableHtmlRendering` is disabled, then use `target.textContent = value;` - * @param target - target element to apply to - * @param val - input value can be either a string or an HTMLElement - * @param options - - * `emptyTarget`, defaults to true, will empty the target. - * `skipEmptyReassignment`, defaults to true, when enabled it will not try to reapply an empty value when the target is already empty + * Processes the provided grid options (mixing in default settings as needed), + * validates required modules (for example, ensuring Sortable.js is loaded if column reordering is enabled), + * and creates all necessary DOM elements for the grid (including header containers, viewports, canvases, panels, etc.). + * It also caches CSS if the container or its ancestors are hidden and calls finish. + * + * @param {Partial} options - Partial grid options to be applied during initialization. */ - applyHtmlCode(target: HTMLElement, val: string | HTMLElement | DocumentFragment, options?: { emptyTarget?: boolean; skipEmptyReassignment?: boolean; }) { - if (target) { - if (val instanceof HTMLElement || val instanceof DocumentFragment) { - // first empty target and then append new HTML element - const emptyTarget = options?.emptyTarget !== false; - if (emptyTarget) { - Utils.emptyElement(target); - } - target.appendChild(val); - } else { - // when it's already empty and we try to reassign empty, it's probably ok to skip the assignment - const skipEmptyReassignment = options?.skipEmptyReassignment !== false; - if (skipEmptyReassignment && !Utils.isDefined(val) && !target.innerHTML) { - return; - } - - let sanitizedText = val; - if (typeof sanitizedText === 'number' || typeof sanitizedText === 'boolean') { - target.textContent = sanitizedText; - } else { - sanitizedText = this.sanitizeHtmlString(val as string); - - // apply HTML when enableHtmlRendering is enabled but make sure we do have a value (without a value, it will simply use `textContent` to clear text content) - if (this._options.enableHtmlRendering && sanitizedText) { - target.innerHTML = sanitizedText; - } else { - target.textContent = sanitizedText; - } - } - } - } - } - protected initialize(options: Partial) { // calculate these only once and share between grid instances if (options?.mixinDefaults) { @@ -875,6 +840,14 @@ export class SlickGrid = Column, O e } } + /** + * Completes grid initialisation by calculating viewport dimensions, measuring cell padding and border differences, + * disabling text selection (except on editable inputs), setting frozen options and pane visibility, + * updating column caches, creating column headers and footers, setting up column sorting, + * creating CSS rules, binding ancestor scroll events, and binding various event handlers + * (e.g. for scrolling, mouse, keyboard, drag-and-drop). + * It also starts up any asynchronous post–render processing if enabled. + */ protected finishInitialization() { if (!this.initialized) { this.initialized = true; @@ -997,7 +970,16 @@ export class SlickGrid = Column, O e } } - /** handles "display:none" on container or container parents, related to issue: https://github.com/6pac/SlickGrid/issues/568 */ + + + + /** + * Finds all container ancestors/parents (including the grid container itself) that are hidden (i.e. have display:none) + * and temporarily applies visible CSS properties (absolute positioning, hidden visibility, block display) + * so that dimensions can be measured correctly. + * It stores the original CSS properties in an internal array for later restoration. + * + * Related to issue: https://github.com/6pac/SlickGrid/issues/568 */ cacheCssForHiddenInit() { this._hiddenParents = Utils.parents(this._container, ':hidden') as HTMLElement[]; this.oldProps = []; @@ -1013,6 +995,12 @@ export class SlickGrid = Column, O e }); } + /** + * Restores the original CSS properties for the container and its hidden + * ancestors that were modified by cacheCssForHiddenInit. + * This ensures that after initial measurements the DOM elements revert + * to their original style settings. + */ restoreCssFromHiddenInit() { // finish handle display:none on container or container parents // - put values back the way they were @@ -1030,17 +1018,24 @@ export class SlickGrid = Column, O e } } - protected hasFrozenColumns() { - return this._options.frozenColumn! > -1; - } - - /** Register an external Plugin */ + /** + * Registers an external plugin to the grid’s internal plugin list. + * Once added, it immediately initialises the plugin by calling its init() + * method with the grid instance. + * @param {T} plugin - The plugin instance to be registered. + */ registerPlugin(plugin: T) { this.plugins.unshift(plugin); plugin.init(this as unknown as SlickGridModel); } - /** Unregister (destroy) an external Plugin */ + /** + * Unregister (destroy) an external Plugin. + * Searches for the specified plugin in the grid’s plugin list. + * When found, it calls the plugin’s destroy() method and removes the plugin from the list, + * thereby unregistering it from the grid. + * @param {T} plugin - The plugin instance to be registered. + */ unregisterPlugin(plugin: SlickPlugin) { for (let i = this.plugins.length; i >= 0; i--) { if (this.plugins[i] === plugin) { @@ -1051,371 +1046,379 @@ export class SlickGrid = Column, O e } } - /** Get a Plugin (addon) by its name */ - getPluginByName

(name: string) { - for (let i = this.plugins.length - 1; i >= 0; i--) { - if (this.plugins[i]?.pluginName === name) { - return this.plugins[i] as P; - } - } - return undefined; - } - - /** - * Unregisters a current selection model and registers a new one. See the definition of SelectionModel for more information. - * @param {Object} selectionModel A SelectionModel. + /** + * Destroy (dispose) of SlickGrid + * + * Unbinds all event handlers, cancels any active cell edits, triggers the onBeforeDestroy event, + * unregisters and destroys plugins, destroys sortable and other interaction instances, + * unbinds ancestor scroll events, removes CSS rules, unbinds events from all key DOM elements + * (canvas, viewports, header, footer, etc.), empties the grid container, removes the grid’s uid class, + * and clears all timers. Optionally, if shouldDestroyAllElements is true, + * calls destroyAllElements to nullify all DOM references. + * + * @param {boolean} shouldDestroyAllElements - do we want to destroy (nullify) all DOM elements as well? This help in avoiding mem leaks */ - setSelectionModel(model: SelectionModel) { - if (this.selectionModel) { - this.selectionModel.onSelectedRangesChanged.unsubscribe(this.handleSelectedRangesChanged.bind(this)); - if (this.selectionModel.destroy) { - this.selectionModel.destroy(); - } - } - - this.selectionModel = model; - if (this.selectionModel) { - this.selectionModel.init(this as unknown as SlickGridModel); - this.selectionModel.onSelectedRangesChanged.subscribe(this.handleSelectedRangesChanged.bind(this)); - } - } - - /** Returns the current SelectionModel. See here for more information about SelectionModels. */ - getSelectionModel() { - return this.selectionModel; - } + destroy(shouldDestroyAllElements?: boolean) { + this._bindingEventService.unbindAll(); + this.slickDraggableInstance = this.destroyAllInstances(this.slickDraggableInstance) as null; + this.slickMouseWheelInstances = this.destroyAllInstances(this.slickMouseWheelInstances) as InteractionBase[]; + this.slickResizableInstances = this.destroyAllInstances(this.slickResizableInstances) as InteractionBase[]; + this.getEditorLock()?.cancelCurrentEdit(); - /** Get Grid Canvas Node DOM Element */ - getCanvasNode(columnIdOrIdx?: number | string, rowIndex?: number) { - return this._getContainerElement(this.getCanvases(), columnIdOrIdx, rowIndex) as HTMLDivElement; - } + this.trigger(this.onBeforeDestroy, {}); - /** Get the canvas DOM element */ - getActiveCanvasNode(e?: Event | SlickEventData_) { - if (e === undefined) { - return this._activeCanvasNode; + let i = this.plugins.length; + while (i--) { + this.unregisterPlugin(this.plugins[i]); } - if (e instanceof SlickEventData) { - e = e.getNativeEvent(); + if (this._options.enableColumnReorder && typeof this.sortableSideLeftInstance?.destroy === 'function') { + this.sortableSideLeftInstance?.destroy(); + this.sortableSideRightInstance?.destroy(); } - this._activeCanvasNode = (e as any)?.target.closest('.grid-canvas'); - return this._activeCanvasNode; - } - - /** Get the canvas DOM element */ - getCanvases() { - return this._canvas; - } - - /** Get the Viewport DOM node element */ - getViewportNode(columnIdOrIdx?: number | string, rowIndex?: number) { - return this._getContainerElement(this.getViewports(), columnIdOrIdx, rowIndex); - } + this.unbindAncestorScrollEvents(); + this._bindingEventService.unbindByEventName(this._container, 'resize'); + this.removeCssRules(); - /** Get all the Viewport node elements */ - getViewports() { - return this._viewport; - } + this._canvas.forEach((element) => { + this._bindingEventService.unbindByEventName(element, 'keydown'); + this._bindingEventService.unbindByEventName(element, 'click'); + this._bindingEventService.unbindByEventName(element, 'dblclick'); + this._bindingEventService.unbindByEventName(element, 'contextmenu'); + this._bindingEventService.unbindByEventName(element, 'mouseover'); + this._bindingEventService.unbindByEventName(element, 'mouseout'); + }); + this._viewport.forEach((view) => { + this._bindingEventService.unbindByEventName(view, 'scroll'); + }); - getActiveViewportNode(e: Event | SlickEventData_) { - this.setActiveViewportNode(e); + this._headerScroller.forEach((el) => { + this._bindingEventService.unbindByEventName(el, 'contextmenu'); + this._bindingEventService.unbindByEventName(el, 'click'); + }); - return this._activeViewportNode; - } + this._headerRowScroller.forEach((scroller) => { + this._bindingEventService.unbindByEventName(scroller, 'scroll'); + }); - /** Sets an active viewport node */ - setActiveViewportNode(e: Event | SlickEventData_) { - if (e instanceof SlickEventData) { - e = e.getNativeEvent(); + if (this._footerRow) { + this._footerRow.forEach((footer) => { + this._bindingEventService.unbindByEventName(footer, 'contextmenu'); + this._bindingEventService.unbindByEventName(footer, 'click'); + }); } - this._activeViewportNode = (e as any)?.target.closest('.slick-viewport'); - return this._activeViewportNode; - } - protected _getContainerElement(targetContainers: HTMLElement[], columnIdOrIdx?: number | string, rowIndex?: number) { - if (!targetContainers) { return; } - if (!columnIdOrIdx) { columnIdOrIdx = 0; } - if (!rowIndex) { rowIndex = 0; } + if (this._footerRowScroller) { + this._footerRowScroller.forEach((scroller) => { + this._bindingEventService.unbindByEventName(scroller, 'scroll'); + }); + } - const idx = (typeof columnIdOrIdx === 'number' ? columnIdOrIdx : this.getColumnIndex(columnIdOrIdx)); + if (this._preHeaderPanelScroller) { + this._bindingEventService.unbindByEventName(this._preHeaderPanelScroller, 'scroll'); + } - const isBottomSide = this.hasFrozenRows && rowIndex >= this.actualFrozenRow + (this._options.frozenBottom ? 0 : 1); - const isRightSide = this.hasFrozenColumns() && idx > this._options.frozenColumn!; + if (this._topHeaderPanelScroller) { + this._bindingEventService.unbindByEventName(this._topHeaderPanelScroller, 'scroll'); + } - return targetContainers[(isBottomSide ? 2 : 0) + (isRightSide ? 1 : 0)]; - } + this._bindingEventService.unbindByEventName(this._focusSink, 'keydown'); + this._bindingEventService.unbindByEventName(this._focusSink2, 'keydown'); - protected measureScrollbar() { - let className = ''; - this._viewport.forEach(v => className += v.className); - const outerdiv = Utils.createDomElement('div', { className, style: { position: 'absolute', top: '-10000px', left: '-10000px', overflow: 'auto', width: '100px', height: '100px' } }, document.body); - const innerdiv = Utils.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; - } + const resizeHandles = this._container.querySelectorAll('.slick-resizable-handle'); + [].forEach.call(resizeHandles, (handle) => { + this._bindingEventService.unbindByEventName(handle, 'dblclick'); + }); - /** Get the headers width in pixel */ - getHeadersWidth() { - this.headersWidth = this.headersWidthL = this.headersWidthR = 0; - const includeScrollbar = !this._options.autoHeight; + const headerColumns = this._container.querySelectorAll('.slick-header-column'); + [].forEach.call(headerColumns, (column) => { + this._bindingEventService.unbindByEventName(column, 'mouseenter'); + this._bindingEventService.unbindByEventName(column, 'mouseleave'); - let i = 0; - const ii = this.columns.length; - for (i = 0; i < ii; i++) { - if (!this.columns[i] || this.columns[i].hidden) { continue; } + this._bindingEventService.unbindByEventName(column, 'mouseenter'); + this._bindingEventService.unbindByEventName(column, 'mouseleave'); + }); - const width = this.columns[i].width; + Utils.emptyElement(this._container); + this._container.classList.remove(this.uid); + this.clearAllTimers(); - if ((this._options.frozenColumn!) > -1 && (i > this._options.frozenColumn!)) { - this.headersWidthR += width || 0; - } else { - this.headersWidthL += width || 0; - } + if (shouldDestroyAllElements) { + this.destroyAllElements(); } + } - if (includeScrollbar) { - if ((this._options.frozenColumn!) > -1 && (i > this._options.frozenColumn!)) { - this.headersWidthR += this.scrollbarDimensions?.width ?? 0; - } else { - this.headersWidthL += this.scrollbarDimensions?.width ?? 0; + /** + * Call destroy method, when exists, on all the instance(s) it found + * + * Given either a single instance or an array of instances (e.g. draggable, mousewheel, resizable), + * pops each one and calls its destroy method if available, then resets the input to an empty array + * (or null for a single instance). Returns the reset value. + * + * @params instances - can be a single instance or a an array of instances + */ + protected destroyAllInstances(inputInstances: null | InteractionBase | Array) { + if (inputInstances) { + const instances = Array.isArray(inputInstances) ? inputInstances : [inputInstances]; + let instance: InteractionBase | undefined; + while (Utils.isDefined(instance = instances.pop())) { + if (instance && typeof instance.destroy === 'function') { + instance.destroy(); + } } } + // reset instance(s) + inputInstances = (Array.isArray(inputInstances) ? [] : null); + return inputInstances; + } - 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; + /** + * Sets all internal references to DOM elements + * (e.g. canvas containers, headers, viewports, focus sinks, etc.) + * to null so that they can be garbage collected. + */ + protected destroyAllElements() { + this._activeCanvasNode = null as any; + this._activeViewportNode = null as any; + this._boundAncestors = null as any; + this._canvas = null as any; + this._canvasTopL = null as any; + this._canvasTopR = null as any; + this._canvasBottomL = null as any; + this._canvasBottomR = null as any; + this._container = null as any; + this._focusSink = null as any; + this._focusSink2 = null as any; + this._groupHeaders = null as any; + this._groupHeadersL = null as any; + this._groupHeadersR = null as any; + this._headerL = null as any; + this._headerR = null as any; + this._headers = null as any; + this._headerRows = null as any; + this._headerRowL = null as any; + this._headerRowR = null as any; + this._headerRowSpacerL = null as any; + this._headerRowSpacerR = null as any; + this._headerRowScrollContainer = null as any; + this._headerRowScroller = null as any; + this._headerRowScrollerL = null as any; + this._headerRowScrollerR = null as any; + this._headerScrollContainer = null as any; + this._headerScroller = null as any; + this._headerScrollerL = null as any; + this._headerScrollerR = null as any; + this._hiddenParents = null as any; + this._footerRow = null as any; + this._footerRowL = null as any; + this._footerRowR = null as any; + this._footerRowSpacerL = null as any; + this._footerRowSpacerR = null as any; + this._footerRowScroller = null as any; + this._footerRowScrollerL = null as any; + this._footerRowScrollerR = null as any; + this._footerRowScrollContainer = null as any; + this._preHeaderPanel = null as any; + this._preHeaderPanelR = null as any; + this._preHeaderPanelScroller = null as any; + this._preHeaderPanelScrollerR = null as any; + this._preHeaderPanelSpacer = null as any; + this._preHeaderPanelSpacerR = null as any; + this._topPanels = null as any; + this._topPanelScrollers = null as any; + this._style = null as any; + this._topPanelScrollerL = null as any; + this._topPanelScrollerR = null as any; + this._topPanelL = null as any; + this._topPanelR = null as any; + this._paneHeaderL = null as any; + this._paneHeaderR = null as any; + this._paneTopL = null as any; + this._paneTopR = null as any; + this._paneBottomL = null as any; + this._paneBottomR = null as any; + this._viewport = null as any; + this._viewportTopL = null as any; + this._viewportTopR = null as any; + this._viewportBottomL = null as any; + this._viewportBottomR = null as any; + this._viewportScrollContainerX = null as any; + this._viewportScrollContainerY = null as any; } - /** Get the grid canvas width */ - getCanvasWidth(): number { - const availableWidth = this.viewportHasVScroll ? this.viewportW - (this.scrollbarDimensions?.width ?? 0) : this.viewportW; - 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; + /** Returns an object containing all of the Grid options set on the grid. See a list of Grid Options here. */ + getOptions() { + return this._options; } - protected updateCanvasWidth(forceColumnWidthsUpdate?: boolean) { - const oldCanvasWidth = this.canvasWidth; - const oldCanvasWidthL = this.canvasWidthL; - const oldCanvasWidthR = this.canvasWidthR; - this.canvasWidth = this.getCanvasWidth(); + /** + * Extends grid options with a given hash. If an there is an active edit, the grid will attempt to commit the changes and only continue if the attempt succeeds. + * @param {Object} options - an object with configuration options. + * @param {Boolean} [suppressRender] - do we want to supress the grid re-rendering? (defaults to false) + * @param {Boolean} [suppressColumnSet] - do we want to supress the columns set, via "setColumns()" method? (defaults to false) + * @param {Boolean} [suppressSetOverflow] - do we want to suppress the call to `setOverflow` + */ + setOptions(newOptions: Partial, suppressRender?: boolean, suppressColumnSet?: boolean, suppressSetOverflow?: boolean): void { + this.prepareForOptionsChange(); - if (this._options.createTopHeaderPanel) { - Utils.width(this._topHeaderPanel, this._options.topHeaderPanelWidth ?? this.canvasWidth); + if (this._options.enableAddRow !== newOptions.enableAddRow) { + this.invalidateRow(this.getDataLength()); } - 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()) { - const cWidth = Utils.width(this._container) || 0; - if (cWidth > 0 && this.canvasWidthL > cWidth && this._options.throwWhenFrozenNotAllViewable) { - throw new Error('[SlickGrid] Frozen columns cannot be wider than the actual grid container width. ' - + 'Make sure to have less columns freezed or make your grid container wider'); - } - 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); + // before applying column freeze, we need our viewports to be scrolled back to left to avoid misaligned column headers + if (newOptions.frozenColumn !== undefined && newOptions.frozenColumn >= 0) { + this.getViewports().forEach(vp => vp.scrollLeft = 0); + this.handleScroll(); // trigger scroll to realign column headers as well + } - if (this._options.createFooterRow) { - Utils.width(this._footerRowScrollerL, this.canvasWidthL); - Utils.width(this._footerRowScrollerR, this.viewportW - this.canvasWidthL); + const originalOptions = Utils.extend(true, {}, this._options); + this._options = Utils.extend(this._options, newOptions); + this.trigger(this.onSetOptions, { optionsBefore: originalOptions, optionsAfter: this._options }); - 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); + this.internal_setOptions(suppressRender, suppressColumnSet, suppressSetOverflow); + } - if (this.hasFrozenRows) { - Utils.width(this._paneBottomL, this.canvasWidthL); - Utils.setStyleSize(this._paneBottomR, 'left', this.canvasWidthL); + /** + * If option.mixinDefaults is true then external code maintains a reference to the options object. In this case there is no need + * to call setOptions() - changes can be made directly to the object. However setOptions() also performs some recalibration of the + * grid in reaction to changed options. activateChangedOptions call the same recalibration routines as setOptions() would have. + * @param {Boolean} [suppressRender] - do we want to supress the grid re-rendering? (defaults to false) + * @param {Boolean} [suppressColumnSet] - do we want to supress the columns set, via "setColumns()" method? (defaults to false) + * @param {Boolean} [suppressSetOverflow] - do we want to suppress the call to `setOverflow` + */ + activateChangedOptions(suppressRender?: boolean, suppressColumnSet?: boolean, suppressSetOverflow?: boolean): void { + this.prepareForOptionsChange(); + this.invalidateRow(this.getDataLength()); - Utils.width(this._viewportBottomL, this.canvasWidthL); - Utils.width(this._viewportBottomR, this.viewportW - this.canvasWidthL); + this.trigger(this.onActivateChangedOptions, { options: this._options }); - 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); + this.internal_setOptions(suppressRender, suppressColumnSet, suppressSetOverflow); + } - if (this._options.createFooterRow) { - Utils.width(this._footerRowScrollerL, '100%'); - Utils.width(this._footerRowL, this.canvasWidth); - } + /** + * Attempts to commit any active cell edit via the editor lock; if successful, calls makeActiveCellNormal to exit edit mode. + * + * @returns {void} - Does not return a value. + */ + protected prepareForOptionsChange() { + if (!this.getEditorLock().commitCurrentEdit()) { + return; + } - if (this._options.createPreHeaderPanel) { - Utils.width(this._preHeaderPanel, this._options.preHeaderPanelWidth ?? this.canvasWidth); - } - Utils.width(this._viewportTopL, '100%'); + this.makeActiveCellNormal(); + } - if (this.hasFrozenRows) { - Utils.width(this._viewportBottomL, '100%'); - Utils.width(this._canvasBottomL, this.canvasWidthL); - } - } + /** + * Depending on new options, sets column header visibility, validates options, sets frozen options, + * forces viewport height recalculation if needed, updates viewport overflow, re-renders the grid (unless suppressed), + * sets the scroller elements, and reinitialises mouse wheel scrolling as needed. + * + * @param {boolean} [suppressRender] - If `true`, prevents the grid from re-rendering. + * @param {boolean} [suppressColumnSet] - If `true`, prevents the columns from being reset. + * @param {boolean} [suppressSetOverflow] - If `true`, prevents updating the viewport overflow setting. + */ + protected internal_setOptions(suppressRender?: boolean, suppressColumnSet?: boolean, suppressSetOverflow?: boolean): void { + if (this._options.showColumnHeader !== undefined) { + this.setColumnHeaderVisibility(this._options.showColumnHeader); } + this.validateAndEnforceOptions(); + this.setFrozenOptions(); - this.viewportHasHScroll = (this.canvasWidth >= this.viewportW - (this.scrollbarDimensions?.width ?? 0)); + // when user changed frozen row option, we need to force a recalculation of each viewport heights + if (this._options.frozenBottom !== undefined) { + this.enforceFrozenRowHeightRecalc = true; + } - 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)); + this._viewport.forEach((view) => { + view.style.overflowY = this._options.autoHeight ? 'hidden' : 'auto'; + }); + if (!suppressRender) { + this.render(); + } - 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)); + this.setScroller(); + if (!suppressSetOverflow) { + this.setOverflow(); } - if (widthChanged || forceColumnWidthsUpdate) { - this.applyColumnWidths(); + if (!suppressColumnSet) { + this.setColumns(this.columns); } - } - protected disableSelection(target: HTMLElement[]) { - target.forEach((el) => { - el.setAttribute('unselectable', 'on'); - (el.style as any).mozUserSelect = 'none'; - this._bindingEventService.bind(el, 'selectstart', () => false); - }); + if (this._options.enableMouseWheelScrollHandler && this._viewport && (!this.slickMouseWheelInstances || this.slickMouseWheelInstances.length === 0)) { + this._viewport.forEach((view) => { + this.slickMouseWheelInstances.push(MouseWheel({ + element: view, + onMouseWheel: this.handleMouseWheel.bind(this) + })); + }); + } else if (this._options.enableMouseWheelScrollHandler === false) { + this.destroyAllInstances(this.slickMouseWheelInstances); // remove scroll handler when option is disable + } } - protected getMaxSupportedCssHeight() { - 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 = Utils.createDomElement('div', { style: { display: 'hidden' } }, document.body); - - while (true) { - const test = supportedHeight * 2; - Utils.height(div, test); - const height = Utils.height(div); + /** + * + * Ensures consistency in option setting, by thastIF autoHeight IS enabled, leaveSpaceForNewRows is set to FALSE. + * And, if forceFitColumns is True, then autosizeColsMode is set to LegacyForceFit. + */ + validateAndEnforceOptions(): void { + if (this._options.autoHeight) { + this._options.leaveSpaceForNewRows = false; + } + if (this._options.forceFitColumns) { + this._options.autosizeColsMode = GridAutosizeColsMode.LegacyForceFit; + } + } - if (test > testUpTo! || height !== test) { - break; - } else { - supportedHeight = test; + /** + * 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) { + if (this.selectionModel) { + this.selectionModel.onSelectedRangesChanged.unsubscribe(this.handleSelectedRangesChanged.bind(this)); + if (this.selectionModel.destroy) { + this.selectionModel.destroy(); } } - div.remove(); - return supportedHeight; - } - - /** Get grid unique identifier */ - getUID() { - return this.uid; + this.selectionModel = model; + if (this.selectionModel) { + this.selectionModel.init(this as unknown as SlickGridModel); + this.selectionModel.onSelectedRangesChanged.subscribe(this.handleSelectedRangesChanged.bind(this)); + } } - /** Get Header Column Width Difference in pixel */ - getHeaderColumnWidthDiff() { - return this.headerColumnWidthDiff; + /** Returns the current SelectionModel. See here for more information about SelectionModels. */ + getSelectionModel() { + return this.selectionModel; } - /** Get scrollbar dimensions */ - getScrollbarDimensions() { - return this.scrollbarDimensions; + /** 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'); + } } - /** Get the displayed scrollbar dimensions */ - getDisplayedScrollbarDimensions() { - return { - width: this.viewportHasVScroll ? (this.scrollbarDimensions?.width ?? 0) : 0, - height: this.viewportHasHScroll ? (this.scrollbarDimensions?.height ?? 0) : 0 - }; - } - /** Get the absolute column minimum width */ - getAbsoluteColumnMinWidth(): number { - return this.absoluteColumnMinWidth; - } + ////////////////////////////////////////////////////////////////////// + // End Grid and DOM Initialisation + ////////////////////////////////////////////////////////////////////// - getPubSubService(): BasePubSub | undefined { - return this._pubSubService; - } - // TODO: this is static. need to handle page mutation. - protected bindAncestorScrollEvents() { - 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)); - } - } - } + ////////////////////////////////////////////////////////////////////// + // Column Management, Headers and Footers + ////////////////////////////////////////////////////////////////////// - protected unbindAncestorScrollEvents() { - this._boundAncestors.forEach((ancestor) => { - this._bindingEventService.unbindByEventName(ancestor, 'scroll'); - }); - this._boundAncestors = []; + // Returns a boolean indicating whether the grid is configured with frozen columns. + protected hasFrozenColumns() { + return this._options.frozenColumn! > -1; } /** @@ -1473,6 +1476,7 @@ export class SlickGrid = Column, O e 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 @@ -1495,26 +1499,6 @@ export class SlickGrid = Column, O e return this.hasFrozenColumns() ? this._footerRow : this._footerRow[0]; } - /** @alias `getPreHeaderPanelLeft` */ - getPreHeaderPanel() { - return this._preHeaderPanel; - } - - /** Get the Pre-Header Panel Left DOM node element */ - getPreHeaderPanelLeft() { - return this._preHeaderPanel; - } - - /** Get the Pre-Header Panel Right DOM node element */ - getPreHeaderPanelRight() { - return this._preHeaderPanelR; - } - - /** Get the Top-Header Panel DOM node element */ - getTopHeaderPanel() { - return this._topHeaderPanel; - } - /** * Get Header Row Column DOM element by its column Id or index * @param {Number|String} columnIdOrIdx - column Id or index @@ -1560,6 +1544,11 @@ export class SlickGrid = Column, O e return footerRowTarget.children[idx] as HTMLDivElement; } + /** + * If footer rows are enabled, clears existing footer cells then iterates over all columns. + * For each visible column, it creates a footer cell element (adding “frozen” classes if needed), + * stores the column definition in the element’s storage, and triggers the onFooterRowCellRendered event. + */ protected createColumnFooter() { if (this._options.createFooterRow) { this._footerRow.forEach((footer) => { @@ -1598,14 +1587,118 @@ export class SlickGrid = Column, O e } } - protected handleHeaderMouseHoverOn(e: Event | SlickEventData_) { - (e as any)?.target.classList.add('ui-state-hover', 'slick-state-hover'); - } + /** + * For each header container, binds a click event that— + * if the clicked header is sortable and no column resizing is in progress— + * --> toggles the sort direction (or adds/removes the column in a multi–column sort), + * --> triggers onBeforeSort + * --> and if not cancelled, updates the sort columns and triggers onSort. + */ + protected setupColumnSort() { + this._headers.forEach((header) => { + this._bindingEventService.bind(header, 'click', (e: any) => { + if (this.columnResizeDragging) { + return; + } - protected handleHeaderMouseHoverOff(e: Event | SlickEventData_) { - (e as any)?.target.classList.remove('ui-state-hover', 'slick-state-hover'); + if (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.columns[this.getColumnIndex(col.columnId)]; + return !tempCol || tempCol.hidden ? null : { columnId: tempCol.id, sortCol: tempCol, sortAsc: col.sortAsc }; + }).filter((el) => el) + }; + } + + if (this.trigger(this.onBeforeSort, onSortArgs, e).getReturnValue() !== false) { + this.setSortColumns(this.sortColumns); + this.trigger(this.onSort, onSortArgs, e); + } + } + }); + }); } + + /** + * Clears any existing header cells and header row cells, recalculates header widths, + * then iterates over each visible column to create header cell elements + * (and header row cells if enabled) with appropriate content, CSS classes, event bindings, + * and sort indicator elements. Also triggers before–destroy and rendered events as needed. + */ protected createColumnHeaders() { this._headers.forEach((header) => { const columnElements = header.querySelectorAll('.slick-header-column'); @@ -1778,112 +1871,20 @@ export class SlickGrid = Column, O e } } - protected setupColumnSort() { - this._headers.forEach((header) => { - this._bindingEventService.bind(header, 'click', (e: any) => { - if (this.columnResizeDragging) { - return; - } + /** + * Destroys any existing sortable instances and creates new ones on the left and right header + * containers using the Sortable library. Configures options including animation, + * drag handle selectors, auto-scroll, and callbacks (onStart, onEnd) that + * update the column order, set columns, trigger onColumnsReordered, and reapply column resizing. + */ + protected setupColumnReorder() { + this.sortableSideLeftInstance?.destroy(); + this.sortableSideRightInstance?.destroy(); - if (e.target.classList.contains('slick-resizable-handle')) { - return; - } + let columnScrollTimer: any = null; - 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.columns[this.getColumnIndex(col.columnId)]; - return !tempCol || tempCol.hidden ? null : { columnId: tempCol.id, sortCol: tempCol, sortAsc: col.sortAsc }; - }).filter((el) => el) - }; - } - - if (this.trigger(this.onBeforeSort, onSortArgs, e).getReturnValue() !== false) { - this.setSortColumns(this.sortColumns); - this.trigger(this.onSort, onSortArgs, e); - } - } - }); - }); - } - - protected setupColumnReorder() { - this.sortableSideLeftInstance?.destroy(); - this.sortableSideRightInstance?.destroy(); - - let columnScrollTimer: any = null; - - const scrollColumnsRight = () => this._viewportScrollContainerX.scrollLeft = this._viewportScrollContainerX.scrollLeft + 10; - const scrollColumnsLeft = () => this._viewportScrollContainerX.scrollLeft = this._viewportScrollContainerX.scrollLeft - 10; + const scrollColumnsRight = () => this._viewportScrollContainerX.scrollLeft = this._viewportScrollContainerX.scrollLeft + 10; + const scrollColumnsLeft = () => this._viewportScrollContainerX.scrollLeft = this._viewportScrollContainerX.scrollLeft - 10; let canDragScroll = false; const sortableOptions = { @@ -1948,17 +1949,36 @@ export class SlickGrid = Column, O e this.sortableSideRightInstance = Sortable.create(this._headerR, sortableOptions); } + /** + * Returns a concatenated array containing the children (header column elements) from both the left and right header containers. + * @returns {HTMLElement[]} - An array of header column elements. + */ protected getHeaderChildren() { const a = Array.from(this._headers[0].children); const b = Array.from(this._headers[1].children); return a.concat(b) as HTMLElement[]; } + /** + * When a resizable handle is double–clicked, extracts the column identifier from the parent element’s id + * (by removing the grid uid) and triggers the onColumnsResizeDblClick event with that identifier. + * @param {MouseEvent & { target: HTMLDivElement }} evt - The double-click event on the resizable handle. + */ protected handleResizeableDoubleClick(evt: MouseEvent & { target: HTMLDivElement; }) { const triggeredByColumn = evt.target.parentElement!.id.replace(this.uid, ''); this.trigger(this.onColumnsResizeDblClick, { triggeredByColumn }); } + /** + * Ensures the Resizable module is available and then iterates over header children to remove + * any existing resizable handles. Determines which columns are resizable (tracking the first + * and last resizable columns) and for each eligible column, creates a resizable handle, + * binds a double–click event, and creates a Resizable instance with callbacks for onResizeStart, + * onResize, and onResizeEnd. These callbacks manage column width adjustments (including force–fit + * and frozen column considerations), update header and canvas widths, trigger related events, + * and re–render the grid as needed. + * @returns {void} + */ protected setupColumnResize() { if (typeof Resizable === 'undefined') { throw new Error(`Slick.Resizable is undefined, make sure to import "slick.interactions.js"`); @@ -2237,1543 +2257,2689 @@ export class SlickGrid = Column, O e c = vc[j]; if (!c || c.hidden) { continue; } - if (this.hasFrozenColumns() && (j > this._options.frozenColumn!)) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - newCanvasWidthR += c.width || 0; - } else { - newCanvasWidthL += c.width || 0; - } - } - } - } + if (this.hasFrozenColumns() && (j > this._options.frozenColumn!)) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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.trigger(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.trigger(this.onBeforeColumnsResize, { triggeredByColumn }).getReturnValue() === true) { + this.applyColumnHeaderWidths(); + } + let newWidth; + for (j = 0; j < vc.length; j++) { + c = vc[j]; + if (!c || c.hidden) { continue; } + newWidth = children[j].offsetWidth; + + if (c.previousWidth !== newWidth && c.rerenderOnResize) { + this.invalidateAllRows(); + } + } + this.updateCanvasWidth(true); + this.render(); + this.trigger(this.onColumnsResized, { triggeredByColumn }); + window.clearTimeout(this._columnResizeTimer); + this._columnResizeTimer = window.setTimeout(() => { this.columnResizeDragging = false; }, 300); + } + }) + ); + } + } + + + /** + * Validates and sets the frozenColumn option (ensuring it is within valid bounds, or setting it to –1) + * and, if a frozenRow is specified (greater than –1), sets the grid’s frozen–row flags, + * computes the frozenRowsHeight (based on rowHeight), and determines the actual frozen row index + * depending on whether frozenBottom is enabled. + */ + protected setFrozenOptions() { + 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; + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + // Column Management - Autosizing + ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Proportionally resize a specific column by its name, index or Id + * + * Resizes based on its content, but determines the column definition from the provided identifier or index. + * Then, obtains a grid canvas and calls getColAutosizeWidth to compute and update the column’s width. + */ + autosizeColumn(columnOrIndexOrId: number | string, isInit?: boolean) { + let colDef: C | null = null; + let colIndex = -1; + if (typeof columnOrIndexOrId === 'number') { + colDef = this.columns[columnOrIndexOrId]; + colIndex = columnOrIndexOrId; + } else if (typeof columnOrIndexOrId === 'string') { + for (let i = 0; i < this.columns.length; i++) { + if (this.columns[i].id === columnOrIndexOrId) { colDef = this.columns[i]; colIndex = i; } + } + } + if (!colDef) { + return; + } + const gridCanvas = this.getCanvasNode(0, 0) as HTMLElement; + this.getColAutosizeWidth(colDef, colIndex, gridCanvas, isInit || false, colIndex); + } + + /** + * Returns true if the column should be treated as locked (i.e. not resized) based on autosize settings. + * The decision is based on whether header text is not ignored, sizeToRemaining is false, + * content size equals header width, and the current width is less than 100 pixels. + * + * @param {AutoSize} [autoSize={}] - The autosize configuration for the column. + * @returns {boolean} - Returns `true` if the column should be treated as locked, otherwise `false`. + */ + protected treatAsLocked(autoSize: AutoSize = {}): boolean { + // treat as locked (don't resize) if small and header is the widest part + return !autoSize.ignoreHeaderText + && !autoSize.sizeToRemaining + && (autoSize.contentSizePx === autoSize.headerWidthPx) + && ((autoSize.widthPx ?? 0) < 100); + } + + /** Proportionately resizes all columns to fill available horizontal space. + * This does not take the cell contents into consideration. + * + * It does this by temporarily caching CSS for hidden containers, calling the internal autosizing logic + * (internalAutosizeColumns) with the autosize mode and initialisation flag, + * then restores the original CSS. + * */ + autosizeColumns(autosizeMode?: string, isInit?: boolean) { + const checkHiddenParents = !(this._hiddenParents?.length); + if (checkHiddenParents) { + this.cacheCssForHiddenInit(); + } + this.internalAutosizeColumns(autosizeMode, isInit); + if (checkHiddenParents) { + this.restoreCssFromHiddenInit(); + } + } + + /** + * Implements the main autosizing algorithm. Depending on the autosize mode, + * it may call legacyAutosizeColumns (for legacy force–fit modes), or proceed + * to compute column widths based on available viewport width. It iterates over columns + * to accumulate total widths, locked widths, and then adjusts widths proportionally. + * Finally, it calls reRenderColumns to update the grid. + * + * @param {string} [autosizeMode] - The autosize mode. If undefined, defaults to `autosizeColsMode` from options. + * @param {boolean} [isInit] - If `true`, applies initial settings for autosizing. + */ + protected internalAutosizeColumns(autosizeMode?: string, isInit?: boolean) { + // LogColWidths(); + autosizeMode = autosizeMode || this._options.autosizeColsMode; + if (autosizeMode === GridAutosizeColsMode.LegacyForceFit || autosizeMode === GridAutosizeColsMode.LegacyOff) { + this.legacyAutosizeColumns(); + return; + } + + if (autosizeMode === GridAutosizeColsMode.None) { + return; + } + + // test for brower canvas support, canvas_context!=null if supported + this.canvas = document.createElement('canvas'); + if (this.canvas?.getContext) { this.canvas_context = this.canvas.getContext('2d'); } + + // pass in the grid canvas + const gridCanvas = this.getCanvasNode(0, 0) as HTMLElement; + const viewportWidth = this.viewportHasVScroll ? this.viewportW - (this.scrollbarDimensions?.width ?? 0) : this.viewportW; + + // iterate columns to get autosizes + let i: number; + let c: C; + let colWidth: number; + let reRender = false; + let totalWidth = 0; + let totalWidthLessSTR = 0; + let strColsMinWidth = 0; + let totalMinWidth = 0; + let totalLockedColWidth = 0; + for (i = 0; i < this.columns.length; i++) { + c = this.columns[i]; + this.getColAutosizeWidth(c, i, gridCanvas, isInit || false, i); + totalLockedColWidth += (c.autoSize?.autosizeMode === ColAutosizeMode.Locked ? (c.width || 0) : (this.treatAsLocked(c.autoSize) ? c.autoSize?.widthPx || 0 : 0)); + totalMinWidth += (c.autoSize?.autosizeMode === ColAutosizeMode.Locked ? (c.width || 0) : (this.treatAsLocked(c.autoSize) ? c.autoSize?.widthPx || 0 : c.minWidth || 0)); + totalWidth += (c.autoSize?.widthPx || 0); + totalWidthLessSTR += (c.autoSize?.sizeToRemaining ? 0 : c.autoSize?.widthPx || 0); + strColsMinWidth += (c.autoSize?.sizeToRemaining ? c.minWidth || 0 : 0); + } + const strColTotalGuideWidth = totalWidth - totalWidthLessSTR; + + if (autosizeMode === GridAutosizeColsMode.FitViewportToCols) { + // - if viewport with is outside MinViewportWidthPx and MaxViewportWidthPx, then the viewport is set to + // MinViewportWidthPx or MaxViewportWidthPx and the FitColsToViewport algorithm is used + // - viewport is resized to fit columns + let setWidth = totalWidth + (this.scrollbarDimensions?.width ?? 0); + autosizeMode = GridAutosizeColsMode.IgnoreViewport; + + if (this._options.viewportMaxWidthPx && setWidth > this._options.viewportMaxWidthPx) { + setWidth = this._options.viewportMaxWidthPx; + autosizeMode = GridAutosizeColsMode.FitColsToViewport; + } else if (this._options.viewportMinWidthPx && setWidth < this._options.viewportMinWidthPx) { + setWidth = this._options.viewportMinWidthPx; + autosizeMode = GridAutosizeColsMode.FitColsToViewport; + } else { + // falling back to IgnoreViewport will size the columns as-is, with render checking + // for (i = 0; i < columns.length; i++) { columns[i].width = columns[i].autoSize.widthPx; } + } + Utils.width(this._container, setWidth); + } + + if (autosizeMode === GridAutosizeColsMode.FitColsToViewport) { + if (strColTotalGuideWidth > 0 && totalWidthLessSTR < viewportWidth - strColsMinWidth) { + // if addl space remains in the viewport and there are SizeToRemaining cols, just the SizeToRemaining cols expand proportionally to fill viewport + for (i = 0; i < this.columns.length; i++) { + c = this.columns[i]; + if (!c || c.hidden) { continue; } + + const totalSTRViewportWidth = viewportWidth - totalWidthLessSTR; + if (c.autoSize?.sizeToRemaining) { + colWidth = totalSTRViewportWidth * (c.autoSize?.widthPx || 0) / strColTotalGuideWidth; + } else { + colWidth = (c.autoSize?.widthPx || 0); + } + if (c.rerenderOnResize && (c.width || 0) !== colWidth) { + reRender = true; + } + c.width = colWidth; + } + } else if ((this._options.viewportSwitchToScrollModeWidthPercent && totalWidthLessSTR + strColsMinWidth > viewportWidth * this._options.viewportSwitchToScrollModeWidthPercent / 100) + || (totalMinWidth > viewportWidth)) { + // if the total columns width is wider than the viewport by switchToScrollModeWidthPercent, switch to IgnoreViewport mode + autosizeMode = GridAutosizeColsMode.IgnoreViewport; + } else { + // otherwise (ie. no SizeToRemaining cols or viewport smaller than columns) all cols other than 'Locked' scale in proportion to fill viewport + // and SizeToRemaining get minWidth + let unallocatedColWidth = totalWidthLessSTR - totalLockedColWidth; + let unallocatedViewportWidth = viewportWidth - totalLockedColWidth - strColsMinWidth; + for (i = 0; i < this.columns.length; i++) { + c = this.columns[i]; + if (!c || c.hidden) { continue; } + + colWidth = c.width || 0; + if (c.autoSize?.autosizeMode !== ColAutosizeMode.Locked && !this.treatAsLocked(c.autoSize)) { + if (c.autoSize?.sizeToRemaining) { + colWidth = c.minWidth || 0; + } else { + // size width proportionally to free space (we know we have enough room due to the earlier calculations) + colWidth = unallocatedViewportWidth / unallocatedColWidth * (c.autoSize?.widthPx || 0) - 1; + if (colWidth < (c.minWidth || 0)) { + colWidth = c.minWidth || 0; + } + + // remove the just allocated widths from the allocation pool + unallocatedColWidth -= (c.autoSize?.widthPx || 0); + unallocatedViewportWidth -= colWidth; + } + } + if (this.treatAsLocked(c.autoSize)) { + colWidth = (c.autoSize?.widthPx || 0); + if (colWidth < (c.minWidth || 0)) { + colWidth = c.minWidth || 0; + } + } + if (c.rerenderOnResize && c.width !== colWidth) { + reRender = true; + } + c.width = colWidth; + } + } + } + + if (autosizeMode === GridAutosizeColsMode.IgnoreViewport) { + // just size columns as-is + for (i = 0; i < this.columns.length; i++) { + if (!this.columns[i] || this.columns[i].hidden) { continue; } + + colWidth = this.columns[i].autoSize?.widthPx || 0; + if (this.columns[i].rerenderOnResize && this.columns[i].width !== colWidth) { + reRender = true; + } + this.columns[i].width = colWidth; + } + } + + this.reRenderColumns(reRender); + } + + /** + * Calculates the ideal autosize width for a given column. First, it sets the default width from the column definition. + * If the autosize mode is not Locked or Guide, then for ContentIntelligent mode it determines the column’s data type + * (handling booleans, numbers, strings, dates, moments) and adjusts autosize settings accordingly. + * It then calls getColContentSize to compute the width needed by the content, applies an additional + * percentage multiplier and padding, clamps to min/max widths, and if in ContentExpandOnly mode ensures + * the width is at least the default width. The computed width is stored in autoSize.widthPx. + * + * @param {C} columnDef - The column definition containing autosize settings and constraints. + * @param {number} colIndex - The index of the column within the grid. + * @param {HTMLElement} gridCanvas - The grid's canvas element where temporary elements will be created. + * @param {boolean} isInit - If `true`, applies initial settings for row selection mode. + * @param {number} colArrayIndex - The index of the column in the column array (used for multi-column adjustments). + */ + protected getColAutosizeWidth(columnDef: C, colIndex: number, gridCanvas: HTMLElement, isInit: boolean, colArrayIndex: number) { + const autoSize = columnDef.autoSize as AutoSize; + + // set to width as default + autoSize.widthPx = columnDef.width; + if (autoSize.autosizeMode === ColAutosizeMode.Locked + || autoSize.autosizeMode === ColAutosizeMode.Guide) { + return; + } + + const dl = this.getDataLength(); // getDataItem(); + const isoDateRegExp = new RegExp(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z/); + + // ContentIntelligent takes settings from column data type + if (autoSize.autosizeMode === ColAutosizeMode.ContentIntelligent) { + // default to column colDataTypeOf (can be used if initially there are no data rows) + let colDataTypeOf = autoSize.colDataTypeOf; + let colDataItem: any; + if (dl > 0) { + const tempRow = this.getDataItem(0); + if (tempRow) { + colDataItem = tempRow[columnDef.field as keyof TData]; + + // check for dates in hiding + if (isoDateRegExp.test(colDataItem)) { colDataItem = Date.parse(colDataItem); } + + colDataTypeOf = typeof colDataItem; + if (colDataTypeOf === 'object') { + if (colDataItem instanceof Date) { colDataTypeOf = 'date'; } + if (typeof moment !== 'undefined' && colDataItem instanceof moment) { colDataTypeOf = 'moment'; } + } + } + } + if (colDataTypeOf === 'boolean') { + autoSize.colValueArray = [true, false]; + } + if (colDataTypeOf === 'number') { + autoSize.valueFilterMode = ValueFilterMode.GetGreatestAndSub; + autoSize.rowSelectionMode = RowSelectionMode.AllRows; + } + if (colDataTypeOf === 'string') { + autoSize.valueFilterMode = ValueFilterMode.GetLongestText; + autoSize.rowSelectionMode = RowSelectionMode.AllRows; + autoSize.allowAddlPercent = 5; + } + if (colDataTypeOf === 'date') { + autoSize.colValueArray = [new Date(2009, 8, 30, 12, 20, 20)]; // Sep 30th 2009, 12:20:20 AM + } + if (colDataTypeOf === 'moment' && typeof moment !== 'undefined') { + autoSize.colValueArray = [moment([2009, 8, 30, 12, 20, 20])]; // Sep 30th 2009, 12:20:20 AM + } + } + + // at this point, the autosizeMode is effectively 'Content', so proceed to get size + let colWidth = autoSize.contentSizePx = this.getColContentSize(columnDef, colIndex, gridCanvas, isInit, colArrayIndex); + + if (colWidth === 0) { + colWidth = autoSize.widthPx || 0; + } + + const addlPercentMultiplier = (autoSize.allowAddlPercent ? (1 + autoSize.allowAddlPercent / 100) : 1); + colWidth = colWidth * addlPercentMultiplier + (this._options.autosizeColPaddingPx || 0); + if (columnDef.minWidth && colWidth < columnDef.minWidth) { colWidth = columnDef.minWidth; } + if (columnDef.maxWidth && colWidth > columnDef.maxWidth) { colWidth = columnDef.maxWidth; } + + if (autoSize.autosizeMode === ColAutosizeMode.ContentExpandOnly || ((columnDef?.editor as any)?.ControlFillsColumn)) { + // only use content width if it's wider than the default column width (this is used for dropdowns and other fixed width controls) + if (colWidth < (columnDef.width || 0)) { + colWidth = columnDef.width || 0; + } + } + autoSize.widthPx = colWidth; + } + + /** + * Determines the width needed to render a column’s content. It first measures the header width (if not ignored) + * and uses it as a baseline. If an explicit colValueArray is provided, it measures that; otherwise, it creates + * a RowInfo object to select a range of rows based on the rowSelectionMode. Depending on the valueFilterMode + * (e.g. DeDuplicate, GetGreatestAndSub, GetLongestTextAndSub, GetLongestText), it adjusts the values to measure. + * It then calls getColWidth (using either canvas text measurement or DOM measurement) and returns the maximum + * of the header width and computed content width (adjusted by a ratio, if applicable). + * + * @param {C} columnDef - The column definition containing formatting and auto-sizing options. + * @param {number} colIndex - The index of the column within the grid. + * @param {HTMLElement} gridCanvas - The grid's canvas element where temporary elements will be created. + * @param {boolean} isInit - If `true`, applies initial row selection mode settings. + * @param {number} colArrayIndex - The index of the column in the column array (used for multi-column adjustments). + * @returns {number} - The computed optimal column width in pixels. + */ + protected getColContentSize(columnDef: C, colIndex: number, gridCanvas: HTMLElement, isInit: boolean, colArrayIndex: number) { + const autoSize = columnDef.autoSize as AutoSize; + let widthAdjustRatio = 1; + + // at this point, the autosizeMode is effectively 'Content', so proceed to get size + + // get header width, if we are taking notice of it + let i: number; + let tempVal: any; + let maxLen = 0; + let maxColWidth = 0; + autoSize.headerWidthPx = 0; + if (!autoSize.ignoreHeaderText) { + autoSize.headerWidthPx = this.getColHeaderWidth(columnDef); + } + if (autoSize.headerWidthPx === 0) { + autoSize.headerWidthPx = (columnDef.width ? columnDef.width + : (columnDef.maxWidth ? columnDef.maxWidth + : (columnDef.minWidth ? columnDef.minWidth : 20) + ) + ); + } + + if (autoSize.colValueArray) { + // if an array of values are specified, just pass them in instead of data + maxColWidth = this.getColWidth(columnDef, gridCanvas, autoSize.colValueArray as any); + return Math.max(autoSize.headerWidthPx, maxColWidth); + } + + // select rows to evaluate using rowSelectionMode and rowSelectionCount + const rowInfo = {} as RowInfo; + rowInfo.colIndex = colIndex; + rowInfo.rowCount = this.getDataLength(); + rowInfo.startIndex = 0; + rowInfo.endIndex = rowInfo.rowCount - 1; + rowInfo.valueArr = null; + rowInfo.getRowVal = (j: number) => this.getDataItem(j)[columnDef.field as keyof TData]; + + const rowSelectionMode = (isInit ? autoSize.rowSelectionModeOnInit : undefined) || autoSize.rowSelectionMode; + + if (rowSelectionMode === RowSelectionMode.FirstRow) { rowInfo.endIndex = 0; } + if (rowSelectionMode === RowSelectionMode.LastRow) { rowInfo.endIndex = rowInfo.startIndex = rowInfo.rowCount - 1; } + if (rowSelectionMode === RowSelectionMode.FirstNRows) { rowInfo.endIndex = Math.min(autoSize.rowSelectionCount || 0, rowInfo.rowCount) - 1; } + + // now use valueFilterMode to further filter selected rows + if (autoSize.valueFilterMode === ValueFilterMode.DeDuplicate) { + const rowsDict: any = {}; + for (i = rowInfo.startIndex; i <= rowInfo.endIndex; i++) { + rowsDict[rowInfo.getRowVal(i)] = true; + } + if (Object.keys) { + rowInfo.valueArr = Object.keys(rowsDict); + } else { + rowInfo.valueArr = []; + for (const v in rowsDict) { + if (rowsDict) { + rowInfo.valueArr.push(v); + } + } + } + rowInfo.startIndex = 0; + rowInfo.endIndex = rowInfo.length - 1; + } + + if (autoSize.valueFilterMode === ValueFilterMode.GetGreatestAndSub) { + // get greatest abs value in data + let maxVal; + let maxAbsVal = 0; + for (i = rowInfo.startIndex; i <= rowInfo.endIndex; i++) { + tempVal = rowInfo.getRowVal(i); + if (Math.abs(tempVal) > maxAbsVal) { + maxVal = tempVal; maxAbsVal = Math.abs(tempVal); + } + } + // now substitute a '9' for all characters (to get widest width) and convert back to a number + maxVal = '' + maxVal; + maxVal = Array(maxVal.length + 1).join('9'); + maxVal = +maxVal; + + rowInfo.valueArr = [maxVal]; + rowInfo.startIndex = rowInfo.endIndex = 0; + } + + if (autoSize.valueFilterMode === ValueFilterMode.GetLongestTextAndSub) { + // get greatest abs value in data + for (i = rowInfo.startIndex; i <= rowInfo.endIndex; i++) { + tempVal = rowInfo.getRowVal(i); + if ((tempVal || '').length > maxLen) { maxLen = tempVal.length; } + } + // now substitute a 'm' for all characters + tempVal = Array(maxLen + 1).join('m'); + widthAdjustRatio = this._options.autosizeTextAvgToMWidthRatio || 0; + + rowInfo.maxLen = maxLen; + rowInfo.valueArr = [tempVal]; + rowInfo.startIndex = rowInfo.endIndex = 0; + } + + if (autoSize.valueFilterMode === ValueFilterMode.GetLongestText) { + // get greatest abs value in data + maxLen = 0; let maxIndex = 0; + for (i = rowInfo.startIndex; i <= rowInfo.endIndex; i++) { + tempVal = rowInfo.getRowVal(i); + if ((tempVal || '').length > maxLen) { maxLen = tempVal.length; maxIndex = i; } + } + // now substitute a 'c' for all characters + tempVal = rowInfo.getRowVal(maxIndex); + rowInfo.maxLen = maxLen; + rowInfo.valueArr = [tempVal]; + rowInfo.startIndex = rowInfo.endIndex = 0; + } + + // !!! HACK !!!! + if (rowInfo.maxLen && rowInfo.maxLen > 30 && colArrayIndex > 1) { autoSize.sizeToRemaining = true; } + maxColWidth = this.getColWidth(columnDef, gridCanvas, rowInfo) * widthAdjustRatio; + return Math.max(autoSize.headerWidthPx, maxColWidth); + } + + /** + * Creates a temporary row and cell element (with absolute positioning, hidden visibility, and nowrap) and iterates + * over the selected rows (as defined in a RowInfo object or provided value array) to render the cell content using + * the column formatter. If in text-only mode and canvas measurement is enabled, uses canvas.measureText; + * otherwise, uses DOM offsetWidth after applying the formatter result to the cell. + * Returns the maximum measured width. + * + * @param {C} columnDef - The column definition containing formatting and auto-sizing options. + * @param {HTMLElement} gridCanvas - The grid's canvas element where the temporary row will be added. + * @param {RowInfo} rowInfo - Object containing row start/end indices and values for width evaluation. + * @returns {number} - The computed optimal column width in pixels. + */ + protected getColWidth(columnDef: C, gridCanvas: HTMLElement, rowInfo: RowInfo) { + const rowEl = Utils.createDomElement('div', { className: 'slick-row ui-widget-content' }, gridCanvas); + const cellEl = Utils.createDomElement('div', { className: 'slick-cell' }, rowEl); + + cellEl.style.position = 'absolute'; + cellEl.style.visibility = 'hidden'; + cellEl.style.textOverflow = 'initial'; + cellEl.style.whiteSpace = 'nowrap'; + + let i: number; + let len: number; + let max = 0; + let maxText = ''; + let formatterResult: string | FormatterResultWithHtml | FormatterResultWithText | HTMLElement | DocumentFragment; + let val: any; + + // get mode - if text only display, use canvas otherwise html element + let useCanvas = (columnDef.autoSize!.widthEvalMode === WidthEvalMode.TextOnly); + + if (columnDef.autoSize?.widthEvalMode === WidthEvalMode.Auto) { + const noFormatter = !columnDef.formatterOverride && !columnDef.formatter; + const formatterIsText = ((columnDef?.formatterOverride as { ReturnsTextOnly: boolean })?.ReturnsTextOnly) + || (!columnDef.formatterOverride && (columnDef.formatter as any)?.ReturnsTextOnly); + useCanvas = noFormatter || formatterIsText; + } + + // use canvas - very fast, but text-only + if (this.canvas_context && useCanvas) { + const style = getComputedStyle(cellEl); + this.canvas_context.font = style.fontSize + ' ' + style.fontFamily; + for (i = rowInfo.startIndex; i <= rowInfo.endIndex; i++) { + // row is either an array or values or a single value + val = (rowInfo.valueArr ? rowInfo.valueArr[i] : rowInfo.getRowVal(i)); + if (columnDef.formatterOverride) { + // use formatterOverride as first preference + formatterResult = (columnDef.formatterOverride as FormatterOverrideCallback)(i, rowInfo.colIndex, val, columnDef, this.getDataItem(i), this as unknown as SlickGridModel); + } else if (columnDef.formatter) { + // otherwise, use formatter + formatterResult = columnDef.formatter(i, rowInfo.colIndex, val, columnDef, this.getDataItem(i), this as unknown as SlickGridModel); + } else { + // otherwise, use plain text + formatterResult = '' + val; + } + len = formatterResult ? this.canvas_context.measureText(formatterResult as string).width : 0; + if (len > max) { + max = len; + maxText = formatterResult as string; + } + } + + cellEl.textContent = maxText; + len = cellEl.offsetWidth; + + rowEl.remove(); + return len; + } + + for (i = rowInfo.startIndex; i <= rowInfo.endIndex; i++) { + val = (rowInfo.valueArr ? rowInfo.valueArr[i] : rowInfo.getRowVal(i)); + if (columnDef.formatterOverride) { + // use formatterOverride as first preference + formatterResult = (columnDef.formatterOverride as FormatterOverrideCallback)(i, rowInfo.colIndex, val, columnDef, this.getDataItem(i), this as unknown as SlickGridModel); + } else if (columnDef.formatter) { + // otherwise, use formatter + formatterResult = columnDef.formatter(i, rowInfo.colIndex, val, columnDef, this.getDataItem(i), this as unknown as SlickGridModel); + } else { + // otherwise, use plain text + formatterResult = '' + val; + } + this.applyFormatResultToCellNode(formatterResult, cellEl); + len = cellEl.offsetWidth; + if (len > max) { max = len; } + } + + rowEl.remove(); + return max; + } + + /** + * Determines the width of a column header by first attempting to find the header element using an ID composed of the + * grid’s uid and the column’s id. If found, clones the element, makes it absolutely positioned and hidden, + * inserts it into the DOM, measures its offsetWidth, and then removes it. If the header element does not exist yet, + * creates a temporary header element with the column’s name and measures its width before removing it. + * Returns the computed header width. + * + * @param {C} columnDef - The column definition containing the header information. + * @returns {number} - The computed width of the column header in pixels. + */ + protected getColHeaderWidth(columnDef: C) { + let width = 0; + // if (columnDef && (!columnDef.resizable || columnDef._autoCalcWidth === true)) { return; } + const headerColElId = this.getUID() + columnDef.id; + let headerColEl = document.getElementById(headerColElId) as HTMLElement; + const dummyHeaderColElId = `${headerColElId}_`; + const clone = headerColEl.cloneNode(true) as HTMLElement; + if (headerColEl) { + // headers have been created, use clone technique + clone.id = dummyHeaderColElId; + clone.style.cssText = 'position: absolute; visibility: hidden;right: auto;text-overflow: initial;white-space: nowrap;'; + headerColEl.parentNode!.insertBefore(clone, headerColEl); + width = clone.offsetWidth; + clone.parentNode!.removeChild(clone); + } else { + // headers have not yet been created, create a new node + const header = this.getHeader(columnDef) as HTMLElement; + headerColEl = Utils.createDomElement('div', { id: dummyHeaderColElId, className: 'ui-state-default slick-state-default slick-header-column' }, header); + const colNameElm = Utils.createDomElement('span', { className: 'slick-column-name' }, headerColEl); + this.applyHtmlCode(colNameElm, columnDef.name!); + clone.style.cssText = 'position: absolute; visibility: hidden;right: auto;text-overflow: initial;white-space: nowrap;'; + if (columnDef.headerCssClass) { + headerColEl.classList.add(...Utils.classNameToList(columnDef.headerCssClass)); + } + width = headerColEl.offsetWidth; + header.removeChild(headerColEl); + } + return width; + } + + /** + * Iterates over all columns to collect current widths (skipping hidden ones), calculates total width + * and available shrink leeway, then enters a “shrink” loop if the total width exceeds the available + * viewport width and a “grow” loop if below. Finally, it applies the computed widths to the columns + * and calls reRenderColumns (with a flag if any width changed) to update the grid. + */ + protected legacyAutosizeColumns() { + let i; + let c: C | undefined; + let shrinkLeeway = 0; + let total = 0; + let prevTotal = 0; + const widths: number[] = []; + const availWidth = this.viewportHasVScroll ? this.viewportW - (this.scrollbarDimensions?.width ?? 0) : this.viewportW; + + for (i = 0; i < this.columns.length; i++) { + c = this.columns[i]; + if (!c || c.hidden) { + widths.push(0); + continue; + } + widths.push(c.width || 0); + total += c.width || 0; + if (c.resizable) { + shrinkLeeway += (c.width || 0) - Math.max((c.minWidth || 0), this.absoluteColumnMinWidth); + } + } + + // shrink + prevTotal = total; + while (total > availWidth && shrinkLeeway) { + const shrinkProportion = (total - availWidth) / shrinkLeeway; + for (i = 0; i < this.columns.length && total > availWidth; i++) { + c = this.columns[i]; + if (!c || c.hidden) { continue; } + const width = widths[i]; + if (!c.resizable || width <= c.minWidth! || width <= this.absoluteColumnMinWidth) { + continue; + } + const absMinWidth = Math.max(c.minWidth!, this.absoluteColumnMinWidth); + let shrinkSize = Math.floor(shrinkProportion * (width - absMinWidth)) || 1; + shrinkSize = Math.min(shrinkSize, width - absMinWidth); + total -= shrinkSize; + shrinkLeeway -= shrinkSize; + widths[i] -= shrinkSize; + } + if (prevTotal <= total) { // avoid infinite loop + break; + } + prevTotal = total; + } + + // grow + prevTotal = total; + while (total < availWidth) { + const growProportion = availWidth / total; + for (i = 0; i < this.columns.length && total < availWidth; i++) { + c = this.columns[i]; + if (!c || c.hidden) { continue; } + const currentWidth = widths[i]; + let growSize; + + if (!c.resizable || c.maxWidth! <= currentWidth) { + growSize = 0; + } else { + growSize = Math.min(Math.floor(growProportion * currentWidth) - currentWidth, (c.maxWidth! - currentWidth) || 1000000) || 1; + } + total += growSize; + widths[i] += (total <= availWidth ? growSize : 0); + } + if (prevTotal >= total) { // avoid infinite loop + break; + } + prevTotal = total; + } + + let reRender = false; + for (i = 0; i < this.columns.length; i++) { + if (!c || c.hidden) { continue; } - if (this.hasFrozenColumns() && newCanvasWidthL !== this.canvasWidthL) { - Utils.width(this._headerL, newCanvasWidthL + 1000); - Utils.setStyleSize(this._paneHeaderR, 'left', newCanvasWidthL); - } + if (this.columns[i].rerenderOnResize && this.columns[i].width !== widths[i]) { + reRender = true; + } + this.columns[i].width = widths[i]; + } - this.applyColumnHeaderWidths(); - if (this._options.syncColumnCellResize) { - this.applyColumnWidths(); - } - this.trigger(this.onColumnsDrag, { - triggeredByColumn: resizeElms.resizeableElement, - resizeHandle: resizeElms.resizeableHandleElement - }); - }, - onResizeEnd: (_e, resizeElms) => { - resizeElms.resizeableElement.classList.remove('slick-header-column-active'); + this.reRenderColumns(reRender); + } - const triggeredByColumn = resizeElms.resizeableElement.id.replace(this.uid, ''); - if (this.trigger(this.onBeforeColumnsResize, { triggeredByColumn }).getReturnValue() === true) { - this.applyColumnHeaderWidths(); - } - let newWidth; - for (j = 0; j < vc.length; j++) { - c = vc[j]; - if (!c || c.hidden) { continue; } - newWidth = children[j].offsetWidth; + /** + * Apply Columns Widths in the UI and optionally invalidate & re-render the columns when specified + * @param {Boolean} shouldReRender - should we invalidate and re-render the grid? + */ + reRenderColumns(reRender?: boolean) { + this.applyColumnHeaderWidths(); + this.updateCanvasWidth(true); - if (c.previousWidth !== newWidth && c.rerenderOnResize) { - this.invalidateAllRows(); - } - } - this.updateCanvasWidth(true); - this.render(); - this.trigger(this.onColumnsResized, { triggeredByColumn }); - window.clearTimeout(this._columnResizeTimer); - this._columnResizeTimer = window.setTimeout(() => { this.columnResizeDragging = false; }, 300); - } - }) - ); + this.trigger(this.onAutosizeColumns, { columns: this.columns }); + + if (reRender) { + this.invalidateAllRows(); + this.render(); } } - protected getVBoxDelta(el: HTMLElement) { - 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; + /** + * Returns an array of column definitions filtered to exclude any that are marked as hidden. + * + * @returns + */ + getVisibleColumns() { + return this.columns.filter(c => !c.hidden); } - protected setFrozenOptions() { - 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(); + /** + * Returns the index of a column with a given id. Since columns can be reordered by the user, this can be used to get the column definition independent of the order: + * @param {String | Number} id A column id. + */ + getColumnIndex(id: number | string): number { + return this.columnsById[id]; + } - this.actualFrozenRow = (this._options.frozenBottom) - ? (dataLength - this._options.frozenRow!) - : this._options.frozenRow!; - } else { - this.hasFrozenRows = false; + /** + * Iterates over the header elements (from both left and right headers) and updates each header’s width based on the + * corresponding visible column’s width minus a computed adjustment (headerColumnWidthDiff). + * Finally, it updates the internal column caches. + * + * @returns + */ + protected applyColumnHeaderWidths() { + if (!this.initialized) { + return; } + + let columnIndex = 0; + const vc = this.getVisibleColumns(); + this._headers.forEach((header) => { + for (let i = 0; i < header.children.length; i++, columnIndex++) { + const h = header.children[i] as HTMLElement; + const col = vc[columnIndex] || {}; + const width = (col.width || 0) - this.headerColumnWidthDiff; + if (Utils.width(h) !== width) { + Utils.width(h, width); + } + } + }); + + this.updateColumnCaches(); } - /** 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'); + /** + * Iterates over all columns (skipping hidden ones) and, for each, retrieves the associated CSS rules + * (using getColumnCssRules). It then sets the left and right CSS properties so that the columns align + * correctly within the grid canvas. It also updates the cumulative offset for non–frozen columns. + */ + protected applyColumnWidths() { + let x = 0; + let w = 0; + let rule: any; + for (let i = 0; i < this.columns.length; i++) { + if (!this.columns[i]?.hidden) { + w = this.columns[i].width || 0; + + rule = this.getColumnCssRules(i); + rule.left.style.left = `${x}px`; + rule.right.style.right = (((this._options.frozenColumn !== -1 && i > this._options.frozenColumn!) ? this.canvasWidthR : this.canvasWidthL) - x - w) + 'px'; + + // If this column is frozen, reset the css left value since the + // column starts in a new viewport. + if (this._options.frozenColumn !== i) { + x += this.columns[i].width!; + } + } + if (this._options.frozenColumn === i) { + x = 0; + } } } - protected setPaneVisibility() { - if (this.hasFrozenColumns()) { - Utils.show(this._paneHeaderR); - Utils.show(this._paneTopR); + /** + * A convenience method that creates a sort configuration for one column (with the given sort direction) + * and calls setSortColumns with it. Accepts a columnId string and an ascending boolean. + * Applies a sort glyph in either ascending or descending form to the header of the column. + * Note that this does not actually sort the column. It only adds the sort glyph to the header. + * + * @param {String | Number} columnId + * @param {Boolean} ascending + */ + setSortColumn(columnId: number | string, ascending: boolean) { + this.setSortColumns([{ columnId, sortAsc: ascending }]); + } - if (this.hasFrozenRows) { - Utils.show(this._paneBottomL); - Utils.show(this._paneBottomR); - } else { - Utils.hide(this._paneBottomR); - Utils.hide(this._paneBottomL); + /** + * Get column by index - iterates over header containers and returns the header column + * element corresponding to the given index. + * + * @param {Number} id - column index + * @returns + */ + getColumnByIndex(id: number) { + let result: HTMLElement | undefined; + this._headers.every((header) => { + const length = header.children.length; + if (id < length) { + result = header.children[id] as HTMLElement; + return false; } - } else { - Utils.hide(this._paneHeaderR); - Utils.hide(this._paneTopR); - Utils.hide(this._paneBottomR); + id -= length; + return true; + }); - if (this.hasFrozenRows) { - Utils.show(this._paneBottomL); - } else { - Utils.hide(this._paneBottomR); - Utils.hide(this._paneBottomL); + return result; + } + + /** + * Accepts an array of objects in the form [ { columnId: [string], sortAsc: [boolean] }, ... ] to + * define the grid's sort order. When called, this will apply a sort glyph in either ascending + * or descending form to the header of each column specified in the array. + * Note that this does not actually sort the column. It only adds the sort glyph to the header. + * + * @param {ColumnSort[]} cols - column sort + */ + setSortColumns(cols: ColumnSort[]) { + this.sortColumns = cols; + + const numberCols = this._options.numberedMultiColumnSort && this.sortColumns.length > 1; + this._headers.forEach((header) => { + let indicators = header.querySelectorAll('.slick-header-column-sorted'); + indicators.forEach((indicator) => { + indicator.classList.remove('slick-header-column-sorted'); + }); + + indicators = header.querySelectorAll('.slick-sort-indicator'); + indicators.forEach((indicator) => { + indicator.classList.remove('slick-sort-indicator-asc'); + indicator.classList.remove('slick-sort-indicator-desc'); + }); + indicators = header.querySelectorAll('.slick-sort-indicator-numbered'); + indicators.forEach((el) => { + el.textContent = ''; + }); + }); + + let i = 1; + this.sortColumns.forEach((col) => { + if (!Utils.isDefined(col.sortAsc)) { + col.sortAsc = true; } - } + + const columnIndex = this.getColumnIndex(col.columnId); + if (Utils.isDefined(columnIndex)) { + const column = this.getColumnByIndex(columnIndex); + if (column) { + column.classList.add('slick-header-column-sorted'); + let indicator = column.querySelector('.slick-sort-indicator'); + indicator?.classList.add(col.sortAsc ? 'slick-sort-indicator-asc' : 'slick-sort-indicator-desc'); + + if (numberCols) { + indicator = column.querySelector('.slick-sort-indicator-numbered'); + if (indicator) { + indicator.textContent = String(i); + } + } + } + } + i++; + }); } - protected setOverflow() { - 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')); + /** Returns the current array of column definitions. */ + getColumns() { + return this.columns; + } - 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')); + /** Get sorted columns representing the current sorting state of the grid **/ + getSortColumns(): ColumnSort[] { + return this.sortColumns; + } + + /** + * Iterates over all columns to compute and store their left and right boundaries + * (based on cumulative widths). Resets the offset when a frozen column is encountered. + */ + protected updateColumnCaches() { + // Pre-calculate cell boundaries. + this.columnPosLeft = []; + this.columnPosRight = []; + let x = 0; + for (let i = 0, ii = this.columns.length; i < ii; i++) { + if (!this.columns[i] || this.columns[i].hidden) { continue; } + + this.columnPosLeft[i] = x; + this.columnPosRight[i] = x + (this.columns[i].width || 0); - if (this._options.viewportClass) { - const viewportClassList = Utils.classNameToList(this._options.viewportClass); - this._viewportTopL.classList.add(...viewportClassList); - this._viewportTopR.classList.add(...viewportClassList); - this._viewportBottomL.classList.add(...viewportClassList); - this._viewportBottomR.classList.add(...viewportClassList); + if (this._options.frozenColumn === i) { + x = 0; + } else { + x += this.columns[i].width || 0; + } } } - protected setScroller() { - if (this.hasFrozenColumns()) { - this._headerScrollContainer = this._headerScrollerR; - this._headerRowScrollContainer = this._headerRowScrollerR; - this._footerRowScrollContainer = this._footerRowScrollerR; + /** + * Iterates over each column to (a) save its original width as widthRequest, + * (b) apply default properties (using mixinDefaults if set) to both the column + * and its autoSize property, (c) update the columnsById mapping, and (d) adjust + * the width if it is less than minWidth or greater than maxWidth. + */ + protected updateColumnProps() { + this.columnsById = {}; + for (let i = 0; i < this.columns.length; i++) { + let m: C = this.columns[i]; + if (m.width) { + m.widthRequest = m.width; + } - if (this.hasFrozenRows) { - if (this._options.frozenBottom) { - this._viewportScrollContainerX = this._viewportBottomR; - this._viewportScrollContainerY = this._viewportTopR; - } else { - this._viewportScrollContainerX = this._viewportScrollContainerY = this._viewportBottomR; - } + if (this._options.mixinDefaults) { + Utils.applyDefaults(m, this._columnDefaults); + if (!m.autoSize) { m.autoSize = {}; } + Utils.applyDefaults(m.autoSize, this._columnAutosizeDefaults); } else { - this._viewportScrollContainerX = this._viewportScrollContainerY = this._viewportTopR; + m = this.columns[i] = Utils.extend({}, this._columnDefaults, m); + m.autoSize = Utils.extend({}, this._columnAutosizeDefaults, m.autoSize); } - } 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; + this.columnsById[m.id] = i; + if (m.minWidth && ((m.width || 0) < m.minWidth)) { + m.width = m.minWidth; + } + if (m.maxWidth && ((m.width || 0) > m.maxWidth)) { + m.width = m.maxWidth; } } } - protected measureCellPaddingAndBorder() { - const h = ['borderLeftWidth', 'borderRightWidth', 'paddingLeft', 'paddingRight']; - const v = ['borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom']; - const header = this._headers[0]; + /** + * Sets grid columns. Column headers will be recreated and all rendered rows will be removed. + * To rerender the grid (if necessary), call render(). + * @param {Column[]} columnDefinitions An array of column definitions. + */ + setColumns(columnDefinitions: C[]) { + this.trigger(this.onBeforeSetColumns, { previousColumns: this.columns, newColumns: columnDefinitions, grid: this }); + this.columns = columnDefinitions; + this.updateColumnsInternal(); + } - this.headerColumnWidthDiff = this.headerColumnHeightDiff = 0; - this.cellWidthDiff = this.cellHeightDiff = 0; + /** Update columns for when a hidden property has changed but the column list itself has not changed. */ + updateColumns() { + this.trigger(this.onBeforeUpdateColumns, { columns: this.columns, grid: this }); + this.updateColumnsInternal(); + } - let el = Utils.createDomElement('div', { className: 'ui-state-default 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(); + /** + * Triggers onBeforeUpdateColumns and calls updateColumnsInternal to update column properties, + * caches, header/footer elements, CSS rules, canvas dimensions, and selections without changing the column array. + */ + protected updateColumnsInternal() { + this.updateColumnProps(); + this.updateColumnCaches(); - const r = Utils.createDomElement('div', { className: 'slick-row' }, this._canvas[0]); - el = Utils.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(); + if (this.initialized) { + this.setPaneFrozenClasses(); + this.setPaneVisibility(); + this.setOverflow(); - this.absoluteColumnMinWidth = Math.max(this.headerColumnWidthDiff, this.cellWidthDiff); + this.invalidateAllRows(); + this.createColumnHeaders(); + this.createColumnFooter(); + this.removeCssRules(); + this.createCssRules(); + this.resizeCanvas(); + this.updateCanvasWidth(); + this.applyColumnHeaderWidths(); + this.applyColumnWidths(); + this.handleScroll(); + this.getSelectionModel()?.refreshSelections(); + } } - protected createCssRules() { - this._style = document.createElement('style'); - this._style.nonce = this._options.nonce || ''; - (this._options.shadowRoot || document.head).appendChild(this._style); + ///////////////////////////////////////////////////////////////////// + /// End Column Management + ///////////////////////////////////////////////////////////////////// - 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; }`, - ]; + ///////////////////////////////////////////////////////////////////// + /// Data Management and Editing + ///////////////////////////////////////////////////////////////////// - const sheet = this._style.sheet; - if (sheet) { - rules.forEach(rule => { - sheet.insertRule(rule); - }); + /** Get Editor lock */ + getEditorLock() { + return this._options.editorLock as SlickEditorLock; + } - for (let i = 0; i < this.columns.length; i++) { - if (!this.columns[i] || this.columns[i].hidden) { continue; } + /** Get Editor Controller */ + getEditController() { + return this.editController; + } - 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); + /** + * Sets a new source for databinding and removes all rendered rows. Note that this doesn't render the new rows - you can follow it with a call to render() to do that. + * @param {CustomDataView|Array<*>} newData New databinding source using a regular JavaScript array.. or a custom object exposing getItem(index) and getLength() functions. + * @param {Number} [scrollToTop] If true, the grid will reset the vertical scroll position to the top of the grid. + */ + setData(newData: CustomDataView | TData[], scrollToTop?: boolean) { + this.data = newData; + this.invalidateAllRows(); + this.updateRowCount(); + if (scrollToTop) { + this.scrollTo(0); } } - /** Create CSS rules via template in case the first approach with createElement('style') doesn't work */ - protected createCssRulesAlternative(rules: string[]) { - const template = document.createElement('template'); - template.innerHTML = '