import { BindingEventService as BindingEventService_, Event as SlickEvent_, Utils as Utils_ } from '../slick.core.js'; import type { GridOption, GridSize, ResizerOption } from '../models/index.js'; import type { SlickGrid } from '../slick.grid.js'; // for (iife) load Slick methods from global Slick object, or use imports for (esm) const BindingEventService = IIFE_ONLY ? Slick.BindingEventService : BindingEventService_; const SlickEvent = IIFE_ONLY ? Slick.Event : SlickEvent_; const Utils = IIFE_ONLY ? Slick.Utils : Utils_; // define some constants, height/width are in pixels const DATAGRID_MIN_HEIGHT = 180; const DATAGRID_MIN_WIDTH = 300; const DATAGRID_BOTTOM_PADDING = 20; /*** * A Resizer plugin that can be used to auto-resize a grid and/or resize with fixed dimensions. * When fixed height is defined, it will auto-resize only the width and vice versa with the width defined. * You can also choose to use the flag "enableAutoSizeColumns" if you want to the plugin to * automatically call the grid "autosizeColumns()" method after each resize. * * USAGE: * * Add the "slick.resizer.js" file and register it with the grid. * * You can specify certain options as arguments when instantiating the plugin like so: * var resizer = new Slick.Plugins.Resizer({ * container: '#gridContainer', * rightPadding: 15, * bottomPadding: 20, * minHeight: 180, * minWidth: 300, * }); * grid.registerPlugin(resizer); * * * The plugin exposes the following events: * * onGridAfterResize: Fired after the grid got resized. You can customize the menu or dismiss it by returning false. * Event args: * grid: Reference to the grid. * dimensions: Resized grid dimensions used * * onGridBeforeResize: Fired before the grid gets resized. You can customize the menu or dismiss it by returning false. * Event args: * grid: Reference to the grid. * * * @param {Object} options available plugin options that can be passed in the constructor: * container: (REQUIRED) DOM element selector of the page container, basically what element in the page will be used to calculate the available space * gridContainer: DOM element selector of the grid container, optional but when provided it will be resized with same size as the grid (typically a container holding the grid and extra custom footer/pagination) * applyResizeToContainer: Defaults to false, do we want to apply the resized dimentions to the grid container as well? * rightPadding: Defaults to 0, right side padding to remove from the total dimension * bottomPadding: Defaults to 20, bottom padding to remove from the total dimension * minHeight: Defaults to 180, minimum height of the grid * minWidth: Defaults to 300, minimum width of the grid * maxHeight: Maximum height of the grid * maxWidth: Maximum width of the grid * calculateAvailableSizeBy: Defaults to "window", which DOM element ("container" or "window") are we using to calculate the available size for the grid? * * @class Slick.Plugins.Resizer */ export class SlickResizer { // -- // public API pluginName = 'Resizer' as const; onGridAfterResize = new SlickEvent<{ grid: SlickGrid; dimensions: GridSize; }>('onGridAfterResize'); onGridBeforeResize = new SlickEvent<{ grid: SlickGrid; }>('onGridBeforeResize'); // -- // protected props protected _bindingEventService: BindingEventService_; protected _fixedHeight?: number | null; protected _fixedWidth?: number | null; protected _grid!: SlickGrid; protected _gridDomElm!: HTMLElement; protected _gridContainerElm!: HTMLElement; protected _pageContainerElm!: HTMLElement; protected _gridOptions!: GridOption; protected _gridUid = ''; protected _lastDimensions?: GridSize; protected _resizePaused = false; protected _timer?: number; protected _options: ResizerOption; protected _defaults: ResizerOption = { bottomPadding: 20, applyResizeToContainer: false, minHeight: 180, minWidth: 300, rightPadding: 0 }; constructor(options: Partial, fixedDimensions?: { height?: number; width?: number; }) { this._bindingEventService = new BindingEventService(); this._options = Utils.extend(true, {}, this._defaults, options); if (fixedDimensions) { this._fixedHeight = fixedDimensions.height; this._fixedWidth = fixedDimensions.width; } } setOptions(newOptions: Partial) { this._options = Utils.extend(true, {}, this._defaults, this._options, newOptions); } init(grid: SlickGrid) { this.setOptions(this._options); this._grid = grid; this._gridOptions = this._grid.getOptions(); this._gridUid = this._grid.getUID(); this._gridDomElm = this._grid.getContainerNode(); this._pageContainerElm = typeof this._options.container === 'string' ? document.querySelector(this._options.container) as HTMLElement : this._options.container as HTMLElement; if (this._options.gridContainer) { this._gridContainerElm = this._options.gridContainer as HTMLElement; } Utils.addSlickEventPubSubWhenDefined(grid.getPubSubService(), this); if (this._gridOptions) { this.bindAutoResizeDataGrid(); } } /** Bind an auto resize trigger on the datagrid, if that is enable then it will resize itself to the available space * Options: we could also provide a % factor to resize on each height/width independently */ bindAutoResizeDataGrid(newSizes?: GridSize) { const gridElmOffset = Utils.offset(this._gridDomElm); // if we can't find the grid to resize, return without binding anything if (this._gridDomElm !== undefined || gridElmOffset !== undefined) { // -- 1st resize the datagrid size at first load (we need this because the .on event is not triggered on first load) // -- also we add a slight delay (in ms) so that we resize after the grid render is done this.resizeGrid(0, newSizes, null); // -- 2nd bind a trigger on the Window DOM element, so that it happens also when resizing after first load // -- bind auto-resize to Window object only if it exist this._bindingEventService.bind(window, 'resize', (event) => { this.onGridBeforeResize.notify({ grid: this._grid }, event, this); // unless the resizer is paused, let's go and resize the grid if (!this._resizePaused) { // for some yet unknown reason, calling the resize twice removes any stuttering/flickering // when changing the height and makes it much smoother experience this.resizeGrid(0, newSizes, event); this.resizeGrid(0, newSizes, event); } }); } } /** * Calculate the datagrid new height/width from the available space, also consider that a % factor might be applied to calculation */ calculateGridNewDimensions(): GridSize | null { const gridElmOffset = Utils.offset(this._gridDomElm); if (!window || this._pageContainerElm === undefined || this._gridDomElm === undefined || gridElmOffset === undefined) { return null; } // calculate bottom padding const bottomPadding = (this._options?.bottomPadding !== undefined) ? this._options.bottomPadding : DATAGRID_BOTTOM_PADDING; let gridHeight = 0; let gridOffsetTop = 0; // which DOM element are we using to calculate the available size for the grid? // defaults to "window" if (this._options.calculateAvailableSizeBy === 'container') { // uses the container's height to calculate grid height without any top offset gridHeight = Utils.innerSize(this._pageContainerElm, 'height') || 0; } else { // uses the browser's window height with its top offset to calculate grid height gridHeight = window.innerHeight || 0; gridOffsetTop = (gridElmOffset !== undefined) ? gridElmOffset.top : 0; } const availableHeight = gridHeight - gridOffsetTop - bottomPadding; const availableWidth = Utils.innerSize(this._pageContainerElm, 'width') || window.innerWidth || 0; const maxHeight = this._options?.maxHeight || undefined; const minHeight = (this._options?.minHeight !== undefined) ? this._options.minHeight : DATAGRID_MIN_HEIGHT; const maxWidth = this._options?.maxWidth || undefined; const minWidth = (this._options?.minWidth !== undefined) ? this._options.minWidth : DATAGRID_MIN_WIDTH; let newHeight = availableHeight; let newWidth = (this._options?.rightPadding) ? availableWidth - this._options.rightPadding : availableWidth; // optionally (when defined), make sure that grid height & width are within their thresholds if (newHeight < minHeight) { newHeight = minHeight; } if (maxHeight && newHeight > maxHeight) { newHeight = maxHeight; } if (newWidth < minWidth) { newWidth = minWidth; } if (maxWidth && newWidth > maxWidth) { newWidth = maxWidth; } // return the new dimensions unless a fixed height/width was defined return { height: this._fixedHeight || newHeight, width: this._fixedWidth || newWidth }; } /** Destroy function when element is destroyed */ destroy() { this.onGridBeforeResize.unsubscribe(); this.onGridAfterResize.unsubscribe(); this._bindingEventService.unbindAll(); } /** * Return the last resize dimensions used by the service * @return {object} last dimensions (height: number, width: number) */ getLastResizeDimensions() { return this._lastDimensions; } /** * Provide the possibility to pause the resizer for some time, until user decides to re-enabled it later if he wish to. * @param {boolean} isResizePaused are we pausing the resizer? */ pauseResizer(isResizePaused: boolean) { this._resizePaused = isResizePaused; } /** * Resize the datagrid to fit the browser height & width. * @param {number} [delay] to wait before resizing, defaults to 0 (in milliseconds) * @param {object} [newSizes] can optionally be passed (height: number, width: number) * @param {object} [event] that triggered the resize, defaults to null * @return If the browser supports it, we can return a Promise that would resolve with the new dimensions */ resizeGrid(delay?: number, newSizes?: GridSize, event?: Event | null): Promise | void { // because of the javascript async nature, we might want to delay the resize a little bit const resizeDelay = delay || 0; // return a Promise when supported by the browser if (typeof Promise === 'function') { return new Promise((resolve) => { if (resizeDelay > 0) { window.clearTimeout(this._timer); this._timer = window.setTimeout(() => { resolve(this.resizeGridCallback(newSizes, event)); }, resizeDelay); } else { resolve(this.resizeGridCallback(newSizes, event)); } }); } else { // OR no return when Promise isn't supported if (resizeDelay > 0) { window.clearTimeout(this._timer); this._timer = window.setTimeout(() => { this.resizeGridCallback(newSizes, event); }, resizeDelay); } else { this.resizeGridCallback(newSizes, event); } } } protected resizeGridCallback(newSizes?: GridSize, event?: Event | null) { const lastDimensions = this.resizeGridWithDimensions(newSizes) as GridSize; this.onGridAfterResize.notify({ grid: this._grid, dimensions: lastDimensions }, event, this); return lastDimensions; } protected resizeGridWithDimensions(newSizes?: GridSize): GridSize | undefined { // calculate the available sizes with minimum height defined as a varant const availableDimensions = this.calculateGridNewDimensions(); if ((newSizes || availableDimensions) && this._gridDomElm) { try { // get the new sizes, if new sizes are passed (not 0), we will use them else use available space // basically if user passes 1 of the dimension, let say he passes just the height, // we will use the height as a fixed height but the width will be resized by it's available space const newHeight = (newSizes?.height) ? newSizes.height : availableDimensions?.height; const newWidth = (newSizes?.width) ? newSizes.width : availableDimensions?.width; // apply these new height/width to the datagrid if (!this._gridOptions.autoHeight) { this._gridDomElm.style.height = `${newHeight}px`; } this._gridDomElm.style.width = `${newWidth}px`; if (this._gridContainerElm) { this._gridContainerElm.style.width = `${newWidth}px`; } // resize the slickgrid canvas on all browser if (this._grid?.resizeCanvas) { this._grid.resizeCanvas(); } // also call the grid auto-size columns so that it takes available when going bigger if (this._gridOptions?.enableAutoSizeColumns && this._grid.autosizeColumns) { // make sure that the grid still exist (by looking if the Grid UID is found in the DOM tree) to avoid SlickGrid error "missing stylesheet" if (this._gridUid && document.querySelector(`.${this._gridUid}`)) { this._grid.autosizeColumns(); } } // keep last resized dimensions & resolve them to the Promise this._lastDimensions = { height: newHeight, width: newWidth }; } catch (e) { this.destroy(); } } return this._lastDimensions; } } // extend Slick namespace on window object when building as iife if (IIFE_ONLY && window.Slick) { Utils.extend(true, window, { Slick: { Plugins: { Resizer: SlickResizer } } }); }