src/data-list/selection.ts (129 lines of code) (raw):
import TableSelection, {type CloneWithConfig, type SelectionItem, type TableSelectionConfig} from '../table/selection';
interface DataListSelectionConfig<T extends SelectionItem> extends TableSelectionConfig<T> {
partialSelected?: Set<T> | undefined;
}
interface DataListCloneWithConfig<T> extends CloneWithConfig<T> {
partialSelected?: Set<T> | undefined;
}
export default class Selection<T extends SelectionItem> extends TableSelection<T> {
protected _partialSelected: Set<T>;
constructor({partialSelected = new Set(), ...props}: DataListSelectionConfig<T>) {
super(props);
this._partialSelected = partialSelected;
}
protected _buildData(data: T[]) {
return new Set(this._getDescendants(data));
}
protected _buildSelected(data: Set<T>, selected: Set<T>) {
// eslint-disable-next-line no-underscore-dangle
const _selected = new Set(selected);
[...data].forEach(item => {
if (_selected.has(item)) {
this._selectDescendants(item, _selected, this._partialSelected);
}
});
return _selected;
}
private _getDescendants(items: readonly T[]) {
let result: T[] = [];
items.forEach(item => {
result.push(item);
result = [...result, ...this._getDescendants(this._getChildren(item))];
});
return result;
}
private _getAncestors(item: T) {
let result: T[] = [];
const parent = [...this._data].find(it => this._getChildren(it).includes(item));
if (parent) {
result = [parent, ...this._getAncestors(parent)];
}
return result;
}
private _selectDescendants(item: T, selected: Set<T>, partialSelected: Set<T>) {
this._getDescendants(this._getChildren(item)).forEach(it => {
selected.add(it);
partialSelected.delete(it);
});
}
private _deselectDescendants(item: T, selected: Set<T>, partialSelected: Set<T>) {
this._getDescendants(this._getChildren(item)).forEach(it => {
selected.delete(it);
partialSelected.delete(it);
});
}
private _selectAncestors(item: T, selected: Set<T>, partialSelected: Set<T>) {
this._getAncestors(item).forEach(ancestor => {
const groupIsSelected = this._getChildren(ancestor)
.filter(it => this._isItemSelectable(it))
.every(it => selected.has(it));
const groupIsPartialSelected = this._getChildren(ancestor)
.filter(it => this._isItemSelectable(it))
.some(it => selected.has(it) || partialSelected.has(it));
if (groupIsSelected) {
selected.add(ancestor);
partialSelected.delete(ancestor);
} else if (groupIsPartialSelected) {
partialSelected.add(ancestor);
}
});
}
private _deselectAncestors(item: T, selected: Set<T>, partialSelected: Set<T>) {
this._getAncestors(item).forEach(ancestor => {
const groupIsPartialSelected = this._getChildren(ancestor)
.filter(it => this._isItemSelectable(it))
.filter(it => it !== item)
.some(it => selected.has(it) || partialSelected.has(it));
if (groupIsPartialSelected) {
partialSelected.add(ancestor);
} else {
partialSelected.delete(ancestor);
}
selected.delete(ancestor);
});
}
select(value = this._focused): Selection<T> {
if (!value || !this._isItemSelectable(value)) {
return this;
}
const selected = new Set(this._selected);
const partialSelected = new Set(this._partialSelected);
selected.add(value);
partialSelected.delete(value);
this._selectDescendants(value, selected, partialSelected);
this._selectAncestors(value, selected, partialSelected);
return this.cloneWith({selected, partialSelected});
}
isPartialSelected(value: T | null): boolean {
return value !== null && value !== undefined && this._partialSelected.has(value);
}
focus(value: T | null | undefined) {
return super.focus(value) as Selection<T>;
}
resetSelection() {
return super.resetSelection() as Selection<T>;
}
cloneWith({partialSelected = this._partialSelected, ...rest}: DataListCloneWithConfig<T>): Selection<T> {
const parentClone = super.cloneWith(rest) as Selection<T>;
return new (this.constructor as typeof Selection)({
// eslint-disable-next-line no-underscore-dangle
data: parentClone._rawData,
// eslint-disable-next-line no-underscore-dangle
selected: parentClone._selected,
// eslint-disable-next-line no-underscore-dangle
focused: parentClone._focused,
// eslint-disable-next-line no-underscore-dangle
getKey: parentClone._getKey,
// eslint-disable-next-line no-underscore-dangle
getChildren: parentClone._getChildren,
// eslint-disable-next-line no-underscore-dangle
isItemSelectable: parentClone._isItemSelectable,
partialSelected,
});
}
deselect(value = this._focused) {
if (!value || !this._isItemSelectable(value)) {
return this;
}
const selected = new Set(this._selected);
const partialSelected = new Set(this._partialSelected);
selected.delete(value);
partialSelected.delete(value);
this._deselectDescendants(value, selected, partialSelected);
this._deselectAncestors(value, selected, partialSelected);
return this.cloneWith({selected, partialSelected});
}
}