slickgrid
Version:
A lightning fast JavaScript grid/spreadsheet
336 lines (297 loc) • 14.3 kB
text/typescript
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<ResizerOption>, 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<ResizerOption>) {
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<GridSize | undefined> | 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
}
}
});
}