UNPKG

@progress/kendo-angular-grid

Version:

Kendo UI Grid for Angular - high performance data grid with paging, filtering, virtualization, CRUD, and more.

437 lines (436 loc) 18 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Injectable, EventEmitter } from '@angular/core'; import { iterator } from '../utils'; import { isPresent, isMultipleRangesEnabled } from '../utils'; import { DomEventsService } from '../common/dom-events.service'; import { LocalDataChangesService } from '../editing/local-data-changes.service'; import { NavigationService } from '../navigation/navigation.service'; import { ContextService } from '../common/provider.service'; import { CellSelectionAggregateService } from '../aggregates/selection-aggregate.service'; import * as i0 from "@angular/core"; import * as i1 from "../common/dom-events.service"; import * as i2 from "../aggregates/selection-aggregate.service"; import * as i3 from "../editing/local-data-changes.service"; import * as i4 from "../navigation/navigation.service"; import * as i5 from "../common/provider.service"; /** * @hidden */ export class SelectionService { domEvents; aggregateService; localDataChangesService; navigationService; ctxService; changes = new EventEmitter(); lastSelectionStartIndex; currentSelection = []; nonSelectableRows = new Map(); selectAllChecked = false; settings; active = false; aggregates; get enableMarquee() { const checkboxOnly = this.settings && typeof this.settings === 'object' && this.settings.checkboxOnly; if (!this.settings || checkboxOnly) { return false; } const selectableSettings = this.settings.selectable; const dragAndMultiple = typeof (selectableSettings) === 'object' && isPresent(selectableSettings) && selectableSettings.mode === 'multiple' && selectableSettings.enabled !== false && !selectableSettings.checkboxOnly && selectableSettings.drag; return this.active && dragAndMultiple; } mouseDownEventArgs; dragging = false; get hasNonSelectable() { return this.nonSelectableRows.size > 0; } cellClickSubscription; mousedownSubscription; dataChangedSubscription; lastSelectionData = {}; _selectAllState; constructor(domEvents, aggregateService, localDataChangesService, navigationService, ctxService) { this.domEvents = domEvents; this.aggregateService = aggregateService; this.localDataChangesService = localDataChangesService; this.navigationService = navigationService; this.ctxService = ctxService; this.addSubscriptions(); } init(settings) { this.settings = settings; if (!isPresent(this.lastSelectionStartIndex)) { this.lastSelectionStartIndex = this.ctxService?.grid.selectionDirective?.rangeSelectionStartRow?.index || 0; this.lastSelectionData = this.ctxService?.grid.selectionDirective?.rangeSelectionStartRow?.dataItem || {}; } this.currentSelection = []; this.nonSelectableRows = new Map(); if (settings.selectable && settings.selectable.enabled !== false) { const iterator = this.getIterator(); this._selectAllState = true; let item = iterator.next(); while (!item.done) { if (item.value && item.value.type === "data") { const rowArgs = { dataItem: item.value.data, index: item.value.index }; if (settings.rowSelected(rowArgs)) { this.currentSelection[item.value.index] = rowArgs; } else { this._selectAllState = undefined; } if (!settings.isRowSelectable(rowArgs)) { this.nonSelectableRows.set(rowArgs.index, rowArgs.dataItem); this._selectAllState = undefined; } } item = iterator.next(); } if (this.currentSelection.length === 0) { this._selectAllState = false; } } } isSelected(index) { if (this.settings && this.active) { return this.options.enabled && isPresent(this.currentSelection[index]) && !this.nonSelectableRows.has(index); } } handleClick(item, event) { if (this.dragging) { this.dragging = false; return; } let ev; const ctrlKey = event.ctrlKey || event.metaKey; if (this.options.mode === "single" && ctrlKey && this.isSelected(item.index)) { ev = this.toggle(item); } else if (this.options.mode === "multiple") { if ((ctrlKey || !this.options.metaKeyMultiSelect) && !event.shiftKey) { ev = this.toggle(item); } else if (event.shiftKey) { const preserveCurrentSelection = isMultipleRangesEnabled(this.settings); ev = this.addAllTo(item, ctrlKey, preserveCurrentSelection); } } if (!isPresent(ev)) { ev = this.select(item); this.currentSelection[item.index] = { dataItem: item.data, index: item.index }; } if (!ev.selectedRows.length && !ev.deselectedRows.length) { return; } ev.ctrlKey = ctrlKey; ev.shiftKey = event.shiftKey; if (this.options.cellAggregates) { ev.cellAggregates = this.aggregateService.onSelectionChange(ev); } if (ev.shiftKey) { ev.rangeStartRow = { dataItem: this.lastSelectionData, index: this.lastSelectionStartIndex }; ev.rangeEndRow = { dataItem: item.data, index: item.index }; } this.changes.emit(ev); } toggle(item) { const selectedRows = []; const deselectedRows = []; this.lastSelectionStartIndex = item.index; this.lastSelectionData = item.data; const rowArgs = { dataItem: item.data, index: item.index }; if (this.isSelected(item.index)) { deselectedRows.push(rowArgs); } else if (!this.nonSelectableRows.has(item.index)) { selectedRows.push(rowArgs); } if (this.hasNonSelectable) { const nonSelectableRows = this.currentSelection.filter(i => this.nonSelectableRows.has(i.index)); deselectedRows.push(...nonSelectableRows); } return { deselectedRows: deselectedRows, selectedRows: selectedRows }; } toggleByIndex(index) { const iterator = this.getIterator(); if (this.selectAllChecked && this.isSelected(index)) { this.selectAllChecked = false; } let item = iterator.next(); while (!item.done) { if (item.value && item.value.type === "data" && item.value.index === index) { const itemToToggle = { data: item.value.data, index: item.value.index }; if (this.isSelected(index) || this.options.mode === "multiple") { return this.toggle(itemToToggle); } else { return this.select(itemToToggle); } } item = iterator.next(); } } select(item) { const deselectedRows = []; const selectedRows = []; this.lastSelectionStartIndex = item.index; this.lastSelectionData = item.data; if (!this.isSelected(item.index) && !this.nonSelectableRows.has(item.index)) { selectedRows.push({ dataItem: item.data, index: item.index }); } this.currentSelection.forEach((row) => { if (row.index !== item.index) { deselectedRows.push(row); } }); return { deselectedRows: deselectedRows, selectedRows: selectedRows }; } //Used to manually deselect removed items deselect(removedItem) { const iterator = this.getIterator(); let item = iterator.next(); while (!item.done) { if (item.value && item.value.type === "data" && item.value.data === removedItem) { const rowArgs = { dataItem: item.value.data, index: item.value.index }; if (this.isSelected(rowArgs.index) || this.nonSelectableRows.has(rowArgs.index)) { const ev = { ctrlKey: false, deselectedRows: [rowArgs], selectedRows: [] }; this.changes.emit(ev); } } item = iterator.next(); } } addAllTo(item, ctrlKey, preserveSelection = false) { const selectedRows = []; const deselectedRows = []; const start = Math.min(this.lastSelectionStartIndex, item.index); const end = Math.max(this.lastSelectionStartIndex, item.index); const iterator = this.getIterator(); let next = iterator.next(); while (!next.done) { if (next.value && next.value.type === "data") { const idx = next.value.index; const rowArgs = { dataItem: next.value.data, index: idx }; if ((idx < start || idx > end) && this.isSelected(idx) && !ctrlKey && !preserveSelection) { deselectedRows.push(rowArgs); } if ((idx >= start && idx <= end) && !this.isSelected(idx) && !this.nonSelectableRows.has(idx)) { selectedRows.push(rowArgs); } } next = iterator.next(); } if (this.hasNonSelectable) { const nonSelectableRows = this.currentSelection.filter(i => this.nonSelectableRows.has(i.index)); deselectedRows.push(...nonSelectableRows); } return { deselectedRows: deselectedRows, selectedRows: selectedRows }; } updateAll(selectAllChecked) { this.selectAllChecked = selectAllChecked; const selectedRows = []; const deselectedRows = []; const iterator = this.getIterator(); let next = iterator.next(); while (!next.done) { if (next.value && next.value.type === "data") { const idx = next.value.index; const rowArgs = { dataItem: next.value.data, index: idx }; if (!this.nonSelectableRows.has(idx)) { if (this.isSelected(idx) && !selectAllChecked) { deselectedRows.push(rowArgs); } if (!this.isSelected(idx) && selectAllChecked) { selectedRows.push(rowArgs); } } } next = iterator.next(); } if (!selectedRows.length && !deselectedRows.length) { return; } if (this.hasNonSelectable) { const nonSelectableRows = this.currentSelection.filter(i => this.nonSelectableRows.has(i.index)); deselectedRows.push(...nonSelectableRows); } const ev = { ctrlKey: true, deselectedRows: deselectedRows, selectedRows: selectedRows, shiftKey: true }; if (this.options.cellAggregates) { ev.cellAggregates = this.aggregateService.onSelectionChange(ev); } this.changes.emit(ev); } selectRange(startIndex, endIndex, preserveSelection, existingSelections = []) { const selectedRows = []; const deselectedRows = []; const start = Math.min(startIndex, endIndex); const end = Math.max(startIndex, endIndex); const iterator = this.getIterator(); let next = iterator.next(); while (!next.done) { if (next.value && next.value.type === "data") { const idx = next.value.index; const rowArgs = { dataItem: next.value.data, index: idx }; if ((idx < start || idx > end) && this.isSelected(idx)) { const deselectRow = !(preserveSelection || existingSelections.find((value) => value && value.dataItem === rowArgs.dataItem && value.index === rowArgs.index)); if (deselectRow) { deselectedRows.push(rowArgs); } } if ((idx >= start && idx <= end) && !this.isSelected(idx) && !this.nonSelectableRows.has(idx)) { selectedRows.push(rowArgs); } } next = iterator.next(); } let cellAggregates; if (this.options.cellAggregates) { cellAggregates = this.aggregateService.onSelectionChange({ selectedRows, deselectedRows }); } if (this.hasNonSelectable) { const nonSelectableRows = this.currentSelection.filter(i => this.nonSelectableRows.has(i.index)); deselectedRows.push(...nonSelectableRows); } return { deselectedRows: deselectedRows, selectedRows: selectedRows, cellAggregates }; } get selectAllState() { return this._selectAllState; } get selected() { return this.currentSelection.map((item) => { return item.index; }).filter((n) => typeof n === "number"); } get options() { const defaultOptions = { cellAggregates: false, checkboxOnly: false, enabled: true, mode: "multiple", metaKeyMultiSelect: true }; if (!isPresent(this.settings)) { return defaultOptions; } if (typeof this.settings.selectable === 'boolean') { return { cellAggregates: false, checkboxOnly: false, enabled: this.settings.selectable, mode: "multiple", metaKeyMultiSelect: true }; } else { return Object.assign(defaultOptions, this.settings.selectable); } } ngOnDestroy() { this.removeSubscriptions(); } targetArgs() { return { index: this.mouseDownEventArgs.rowIndex, dataItem: this.mouseDownEventArgs.dataItem }; } addSubscriptions() { if (!this.cellClickSubscription) { this.cellClickSubscription = this.domEvents.cellClick.subscribe((args) => { if (this.options.enabled && !this.options.checkboxOnly && args.type !== 'contextmenu') { if (this.active) { this.handleClick({ index: args.rowIndex, data: args.dataItem }, args.originalEvent); } } }); } if (!this.mousedownSubscription) { this.mousedownSubscription = this.domEvents.cellMousedown.subscribe((args) => { this.mouseDownEventArgs = args; if ((this.options.enabled && (!this.options.mode || this.options.mode === "multiple") && !this.options.checkboxOnly && args.originalEvent.shiftKey)) { if (this.active) { args.originalEvent.preventDefault(); this.navigationService.focusCellByElement(args.originalEvent.target); } } }); } if (this.localDataChangesService && !this.dataChangedSubscription) { this.dataChangedSubscription = this.localDataChangesService.changes.subscribe((args) => { if (this.active) { if (isPresent(args.action) && args.action === 'remove') { this.deselect(args.item); } } }); } } getIterator() { const accessor = this.settings.view.accessor(); if (!accessor) { return; } return accessor[iterator](); } removeSubscriptions() { if (this.cellClickSubscription) { this.cellClickSubscription.unsubscribe(); this.cellClickSubscription = null; } if (this.mousedownSubscription) { this.mousedownSubscription.unsubscribe(); this.mousedownSubscription = null; } if (this.dataChangedSubscription) { this.dataChangedSubscription.unsubscribe(); this.dataChangedSubscription = null; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SelectionService, deps: [{ token: i1.DomEventsService }, { token: i2.CellSelectionAggregateService }, { token: i3.LocalDataChangesService }, { token: i4.NavigationService }, { token: i5.ContextService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SelectionService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SelectionService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i1.DomEventsService }, { type: i2.CellSelectionAggregateService }, { type: i3.LocalDataChangesService }, { type: i4.NavigationService }, { type: i5.ContextService }]; } });