import { BindingEventService } from '@slickgrid-universal/binding'; import type { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; import { createDomElement, emptyElement } from '@slickgrid-universal/utils'; import { SlickEventHandler, type SlickDataView, type SlickEventData, type SlickGrid } from '../core/index.js'; import type { SelectionModel } from '../enums/index.js'; import type { CheckboxSelectorOption, Column, DOMMouseOrTouchEvent, GridOption, OnHeaderClickEventArgs, OnKeyDownEventArgs, SelectableOverrideCallback, } from '../interfaces/index.js'; import { createDocumentFragmentOrElement } from '../services/utilities.js'; import { SlickHybridSelectionModel } from './slickHybridSelectionModel.js'; export interface RowLookup { [row: number]: boolean; } const CHECK_ICON = 'sgi-icon-check'; const UNCHECK_ICON = 'sgi-icon-uncheck'; const PARTIAL_CHECK_ICON = 'sgi-icon-partial-check'; const DEFAULT_COLUMN_ID = '_checkbox_selector'; export class SlickCheckboxSelectColumn { readonly pluginName = 'CheckboxSelectColumn'; protected _defaults = { columnId: DEFAULT_COLUMN_ID, cssClass: null, field: DEFAULT_COLUMN_ID, hideSelectAllCheckbox: false, name: '', toolTip: 'Select/Deselect All', width: 30, reorderable: false, applySelectOnAllPages: true, // when that is enabled the "Select All" will be applied to all pages (when using Pagination) hideInColumnTitleRow: false, hideInFilterHeaderRow: true, } as unknown as CheckboxSelectorOption; protected _addonOptions: CheckboxSelectorOption = this._defaults; protected _bindEventService: BindingEventService; protected _checkboxColumnCellIndex: number | null = null; protected _dataView!: SlickDataView; protected _eventHandler: SlickEventHandler; protected _headerRowNode?: HTMLElement; protected _grid!: SlickGrid; protected _isSelectAllChecked = false; protected _isPartialSelectAllChecked = false; protected _isUsingDataView = false; protected _rowSelectionModel?: SelectionModel; protected _selectableOverride?: SelectableOverrideCallback | number; protected _selectAll_UID: number; protected _selectedRowsLookup: RowLookup = {}; protected _timer?: any; constructor( protected readonly pubSubService: BasePubSubService, options?: CheckboxSelectorOption ) { this._selectAll_UID = this.createUID(); this._bindEventService = new BindingEventService(); this._eventHandler = new SlickEventHandler(); this._addonOptions = { ...this._defaults, ...options } as CheckboxSelectorOption; } get addonOptions(): CheckboxSelectorOption { return this._addonOptions; } get headerRowNode(): HTMLElement | undefined { return this._headerRowNode; } /** Getter for the Grid Options pulled through the Grid Object */ get gridOptions(): GridOption { return this._grid?.getOptions() ?? {}; } get selectAllUid(): number { return this._selectAll_UID; } set selectedRowsLookup(selectedRows: RowLookup) { this._selectedRowsLookup = selectedRows; } init(grid: SlickGrid): void { this._grid = grid; this._isUsingDataView = !Array.isArray(grid.getData()); if (this._isUsingDataView) { this._dataView = grid.getData(); } // we cannot apply "Select All" to all pages when using a Backend Service API (OData, GraphQL, ...) if (this.gridOptions.backendServiceApi) { this._addonOptions.applySelectOnAllPages = false; } this._eventHandler .subscribe(grid.onSelectedRowsChanged, this.handleSelectedRowsChanged.bind(this)) .subscribe(grid.onClick, this.handleClick.bind(this)) .subscribe(grid.onKeyDown, this.handleKeyDown.bind(this)) // whenever columns changed or is (re)created, we need to rerender Select All checkbox .subscribe(grid.onAfterSetColumns, this.handleDataViewSelectedIdsChanged.bind(this)) .subscribe(grid.onAfterUpdateColumns, this.handleDataViewSelectedIdsChanged.bind(this)); if (this._isUsingDataView && this._dataView && this._addonOptions.applySelectOnAllPages) { this._eventHandler .subscribe(this._dataView.onSelectedRowIdsChanged, this.handleDataViewSelectedIdsChanged.bind(this)) .subscribe(this._dataView.onPagingInfoChanged, this.handleDataViewSelectedIdsChanged.bind(this)); } if (!this._addonOptions.hideInFilterHeaderRow) { this.addCheckboxToFilterHeaderRow(grid); } if (!this._addonOptions.hideInColumnTitleRow) { this._eventHandler .subscribe(this._grid.onHeaderClick, this.handleHeaderClick.bind(this)) .subscribe(this._grid.onHeaderKeyDown, (_e, args) => this.handleHeaderKeyDown(args.event, args)); } // this also requires the Row Selection Model to be registered as well if (!this._rowSelectionModel || !this._grid.getSelectionModel()) { const selectionType = this.gridOptions.selectionOptions?.selectionType || 'row'; this._rowSelectionModel = grid.getSelectionModel() ?? new SlickHybridSelectionModel({ ...this.gridOptions.selectionOptions, selectionType }); this._grid.setSelectionModel(this._rowSelectionModel); } // user might want to pre-select some rows // the setTimeout is because of timing issue with styling (row selection happen but rows aren't highlighted properly) if (this.gridOptions.preselectedRows && this._rowSelectionModel && this._grid.getSelectionModel()) { clearTimeout(this._timer); this._timer = setTimeout(() => this.selectRows(this.gridOptions.preselectedRows || [])); } // user could override the checkbox icon logic from within the options or after instantiating the plugin if (typeof this._addonOptions.selectableOverride === 'function') { this.selectableOverride(this._addonOptions.selectableOverride); } } dispose(): void { clearTimeout(this._timer); this._bindEventService.unbindAll(); this._eventHandler.unsubscribeAll(); } /** * Create the plugin before the Grid creation to avoid having odd behaviors. * Mostly because the column definitions might change after the grid creation, so we want to make sure to add it before then */ create(columns: Column[], gridOptions: GridOption): SlickCheckboxSelectColumn | null { this._addonOptions = { ...this._defaults, ...gridOptions.checkboxSelector } as CheckboxSelectorOption; if (Array.isArray(columns) && gridOptions) { const selectionColumn: Column = this.getColumnDefinition(); // add new checkbox column unless it was already added if (!columns.some((col) => col.id === selectionColumn.id)) { // column index position in the grid const columnPosition = gridOptions?.checkboxSelector?.columnIndexPosition ?? 0; if (columnPosition > 0) { columns.splice(columnPosition, 0, selectionColumn); } else { columns.unshift(selectionColumn); } this.pubSubService.publish('onPluginColumnsChanged', { columns, pluginName: this.pluginName, }); } } return this; } getOptions(): CheckboxSelectorOption { return this._addonOptions; } setOptions(options: CheckboxSelectorOption): void { this._addonOptions = { ...this._addonOptions, ...options } as CheckboxSelectorOption; if (this._addonOptions.hideSelectAllCheckbox) { this.hideSelectAllFromColumnHeaderTitleRow(); this.hideSelectAllFromColumnHeaderFilterRow(); } else { if (!this._addonOptions.hideInColumnTitleRow) { this.renderSelectAllCheckbox(this._isSelectAllChecked, this._isPartialSelectAllChecked); this._eventHandler.subscribe(this._grid.onHeaderClick, this.handleHeaderClick.bind(this)); } else { this.hideSelectAllFromColumnHeaderTitleRow(); if (this._addonOptions.name) { this._grid.updateColumnHeader(this._addonOptions.columnId || '', this._addonOptions.name, ''); } } if (!this._addonOptions.hideInFilterHeaderRow) { const selectAllContainerElm = this.headerRowNode?.querySelector('#filter-checkbox-selectall-container'); if (selectAllContainerElm) { selectAllContainerElm.style.display = 'flex'; selectAllContainerElm.ariaChecked = String(this._isSelectAllChecked); const selectAllInputElm = selectAllContainerElm.querySelector('input[type="checkbox"]'); if (selectAllInputElm) { selectAllInputElm.ariaChecked = String(this._isSelectAllChecked); selectAllInputElm.checked = this._isSelectAllChecked; } } } else { this.hideSelectAllFromColumnHeaderFilterRow(); } } } deSelectRows(rowArray: number[]): void { const removeRows: number[] = []; for (const row of rowArray) { if (this._selectedRowsLookup[row]) { removeRows[removeRows.length] = row; } } this._grid.setSelectedRows( this._grid.getSelectedRows().filter((n) => removeRows.indexOf(n) < 0), 'SlickCheckboxSelectColumn.deSelectRows' ); } selectRows(rowArray: number[]): void { const addRows = []; for (const row of rowArray) { if (this._selectedRowsLookup[row]) { addRows[addRows.length] = row; } } const newSelectedRows = this._grid.getSelectedRows()?.concat(addRows); this._grid.setSelectedRows(newSelectedRows); } /** * use a DocumentFragment to return a fragment including an then a