@mui/x-tree-view
Version:
The community edition of the MUI X Tree View components.
327 lines (317 loc) • 12.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TreeViewSelectionPlugin = void 0;
exports.getLookupFromArray = getLookupFromArray;
var _empty = require("@base-ui/utils/empty");
var _items = require("../items");
var _selectors = require("./selectors");
var _itemPlugin = require("./itemPlugin");
var _tree = require("../../utils/tree");
class TreeViewSelectionPlugin {
lastSelectedItem = null;
lastSelectedRange = {};
// We can't type `store`, otherwise we get the following TS error:
// 'selection' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
constructor(store) {
this.store = store;
store.itemPluginManager.register(_itemPlugin.useSelectionItemPlugin, null);
}
setSelectedItems = (event, newModel, additionalItemsToPropagate) => {
const {
selectionPropagation = _empty.EMPTY_OBJECT,
selectedItems,
onItemSelectionToggle,
onSelectedItemsChange
} = this.store.parameters;
const oldModel = _selectors.selectionSelectors.selectedItemsRaw(this.store.state);
let cleanModel;
const isMultiSelectEnabled = _selectors.selectionSelectors.isMultiSelectEnabled(this.store.state);
if (isMultiSelectEnabled && (selectionPropagation.descendants || selectionPropagation.parents)) {
cleanModel = propagateSelection({
store: this.store,
selectionPropagation,
newModel: newModel,
oldModel: oldModel,
additionalItemsToPropagate
});
} else {
cleanModel = newModel;
}
if (onItemSelectionToggle) {
if (isMultiSelectEnabled) {
const changes = getAddedAndRemovedItems({
store: this.store,
newModel: cleanModel,
oldModel: oldModel
});
if (onItemSelectionToggle) {
changes.added.forEach(itemId => {
onItemSelectionToggle(event, itemId, true);
});
changes.removed.forEach(itemId => {
onItemSelectionToggle(event, itemId, false);
});
}
} else if (cleanModel !== oldModel) {
if (oldModel != null) {
onItemSelectionToggle(event, oldModel, false);
}
if (cleanModel != null) {
onItemSelectionToggle(event, cleanModel, true);
}
}
}
if (selectedItems === undefined) {
this.store.set('selectedItems', cleanModel);
}
onSelectedItemsChange?.(event, cleanModel);
};
selectRange = (event, [start, end]) => {
const isMultiSelectEnabled = _selectors.selectionSelectors.isMultiSelectEnabled(this.store.state);
if (!isMultiSelectEnabled) {
return;
}
let newSelectedItems = _selectors.selectionSelectors.selectedItems(this.store.state).slice();
// If the last selection was a range selection,
// remove the items that were part of the last range from the model
if (Object.keys(this.lastSelectedRange).length > 0) {
newSelectedItems = newSelectedItems.filter(id => !this.lastSelectedRange[id]);
}
// Add to the model the items that are part of the new range and not already part of the model.
const selectedItemsLookup = getLookupFromArray(newSelectedItems);
const range = (0, _tree.getNonDisabledItemsInRange)(this.store.state, start, end).filter(id => _selectors.selectionSelectors.isItemSelectable(this.store.state, id));
const itemsToAddToModel = range.filter(id => !selectedItemsLookup[id]);
newSelectedItems = newSelectedItems.concat(itemsToAddToModel);
this.setSelectedItems(event, newSelectedItems);
this.lastSelectedRange = getLookupFromArray(range);
};
buildPublicAPI = () => {
return {
setItemSelection: this.setItemSelection
};
};
/**
* Select or deselect an item.
* @param {object} parameters The parameters of the method.
* @param {TreeViewItemId} parameters.itemId The id of the item to select or deselect.
* @param {React.SyntheticEvent} parameters.event The DOM event that triggered the change.
* @param {boolean} parameters.keepExistingSelection If `true`, the other already selected items will remain selected, otherwise, they will be deselected. This parameter is only relevant when `multiSelect` is `true`
* @param {boolean | undefined} parameters.shouldBeSelected If `true` the item will be selected. If `false` the item will be deselected. If not defined, the item's selection status will be toggled.
*/
setItemSelection = ({
itemId,
event = null,
keepExistingSelection = false,
shouldBeSelected
}) => {
if (!_selectors.selectionSelectors.enabled(this.store.state)) {
return;
}
let newSelected;
const isMultiSelectEnabled = _selectors.selectionSelectors.isMultiSelectEnabled(this.store.state);
if (keepExistingSelection) {
const oldSelected = _selectors.selectionSelectors.selectedItems(this.store.state);
const isSelectedBefore = _selectors.selectionSelectors.isItemSelected(this.store.state, itemId);
if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) {
newSelected = oldSelected.filter(id => id !== itemId);
} else if (!isSelectedBefore && (shouldBeSelected === true || shouldBeSelected == null)) {
newSelected = [itemId].concat(oldSelected);
} else {
newSelected = oldSelected;
}
} else {
// eslint-disable-next-line no-lonely-if
if (shouldBeSelected === false || shouldBeSelected == null && _selectors.selectionSelectors.isItemSelected(this.store.state, itemId)) {
newSelected = isMultiSelectEnabled ? [] : null;
} else {
newSelected = isMultiSelectEnabled ? [itemId] : itemId;
}
}
this.setSelectedItems(event, newSelected,
// If shouldBeSelected === selectionSelectors.isItemSelected(store, itemId), we still want to propagate the select.
// This is useful when the element is in an indeterminate state.
[itemId]);
this.lastSelectedItem = itemId;
this.lastSelectedRange = {};
};
/**
* Select all the navigable items in the tree.
* @param {React.SyntheticEvent} event The DOM event that triggered the change.
*/
selectAllNavigableItems = event => {
const isMultiSelectEnabled = _selectors.selectionSelectors.isMultiSelectEnabled(this.store.state);
if (!isMultiSelectEnabled) {
return;
}
const navigableItems = (0, _tree.getAllNavigableItems)(this.store.state);
this.setSelectedItems(event, navigableItems);
this.lastSelectedRange = getLookupFromArray(navigableItems);
};
/**
* Expand the current selection range up to the given item.
* @param {React.SyntheticEvent} event The DOM event that triggered the change.
* @param {TreeViewItemId} itemId The id of the item to expand the selection to.
*/
expandSelectionRange = (event, itemId) => {
if (this.lastSelectedItem != null) {
const [start, end] = (0, _tree.findOrderInTremauxTree)(this.store.state, itemId, this.lastSelectedItem);
this.selectRange(event, [start, end]);
}
};
/**
* Expand the current selection range from the first navigable item to the given item.
* @param {React.SyntheticEvent} event The DOM event that triggered the change.
* @param {TreeViewItemId} itemId The id of the item up to which the selection range should be expanded.
*/
selectRangeFromStartToItem = (event, itemId) => {
this.selectRange(event, [(0, _tree.getFirstNavigableItem)(this.store.state), itemId]);
};
/**
* Expand the current selection range from the given item to the last navigable item.
* @param {React.SyntheticEvent} event The DOM event that triggered the change.
* @param {TreeViewItemId} itemId The id of the item from which the selection range should be expanded.
*/
selectRangeFromItemToEnd = (event, itemId) => {
this.selectRange(event, [itemId, (0, _tree.getLastNavigableItem)(this.store.state)]);
};
/**
* Update the selection when navigating with ArrowUp / ArrowDown keys.
* @param {React.SyntheticEvent} event The DOM event that triggered the change.
* @param {TreeViewItemId} currentItemId The id of the active item before the keyboard navigation.
* @param {TreeViewItemId} nextItemId The id of the active item after the keyboard navigation.
*/
selectItemFromArrowNavigation = (event, currentItem, nextItem) => {
const isMultiSelectEnabled = _selectors.selectionSelectors.isMultiSelectEnabled(this.store.state);
if (!isMultiSelectEnabled) {
return;
}
let newSelectedItems = _selectors.selectionSelectors.selectedItems(this.store.state).slice();
if (Object.keys(this.lastSelectedRange).length === 0) {
newSelectedItems.push(nextItem);
this.lastSelectedRange = {
[currentItem]: true,
[nextItem]: true
};
} else {
if (!this.lastSelectedRange[currentItem]) {
this.lastSelectedRange = {};
}
if (this.lastSelectedRange[nextItem]) {
newSelectedItems = newSelectedItems.filter(id => id !== currentItem);
delete this.lastSelectedRange[currentItem];
} else {
newSelectedItems.push(nextItem);
this.lastSelectedRange[nextItem] = true;
}
}
this.setSelectedItems(event, newSelectedItems);
};
}
exports.TreeViewSelectionPlugin = TreeViewSelectionPlugin;
function propagateSelection({
store,
selectionPropagation,
newModel,
oldModel,
additionalItemsToPropagate
}) {
if (!selectionPropagation.descendants && !selectionPropagation.parents) {
return newModel;
}
let shouldRegenerateModel = false;
const newModelLookup = getLookupFromArray(newModel);
const changes = getAddedAndRemovedItems({
store,
newModel,
oldModel
});
additionalItemsToPropagate?.forEach(itemId => {
if (newModelLookup[itemId]) {
if (!changes.added.includes(itemId)) {
changes.added.push(itemId);
}
} else if (!changes.removed.includes(itemId)) {
changes.removed.push(itemId);
}
});
changes.added.forEach(addedItemId => {
if (selectionPropagation.descendants) {
const selectDescendants = itemId => {
if (itemId !== addedItemId) {
shouldRegenerateModel = true;
newModelLookup[itemId] = true;
}
_items.itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(selectDescendants);
};
selectDescendants(addedItemId);
}
if (selectionPropagation.parents) {
const checkAllDescendantsSelected = itemId => {
if (!newModelLookup[itemId]) {
return false;
}
const children = _items.itemsSelectors.itemOrderedChildrenIds(store.state, itemId);
return children.every(checkAllDescendantsSelected);
};
const selectParents = itemId => {
const parentId = _items.itemsSelectors.itemParentId(store.state, itemId);
if (parentId == null) {
return;
}
const siblings = _items.itemsSelectors.itemOrderedChildrenIds(store.state, parentId);
if (siblings.every(checkAllDescendantsSelected)) {
shouldRegenerateModel = true;
newModelLookup[parentId] = true;
selectParents(parentId);
}
};
selectParents(addedItemId);
}
});
changes.removed.forEach(removedItemId => {
if (selectionPropagation.parents) {
let parentId = _items.itemsSelectors.itemParentId(store.state, removedItemId);
while (parentId != null) {
if (newModelLookup[parentId]) {
shouldRegenerateModel = true;
delete newModelLookup[parentId];
}
parentId = _items.itemsSelectors.itemParentId(store.state, parentId);
}
}
if (selectionPropagation.descendants) {
const deSelectDescendants = itemId => {
if (itemId !== removedItemId) {
shouldRegenerateModel = true;
delete newModelLookup[itemId];
}
_items.itemsSelectors.itemOrderedChildrenIds(store.state, itemId).forEach(deSelectDescendants);
};
deSelectDescendants(removedItemId);
}
});
return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel;
}
function getAddedAndRemovedItems({
store,
oldModel,
newModel
}) {
const newModelMap = new Map();
newModel.forEach(id => {
newModelMap.set(id, true);
});
return {
added: newModel.filter(itemId => !_selectors.selectionSelectors.isItemSelected(store.state, itemId)),
removed: oldModel.filter(itemId => !newModelMap.has(itemId))
};
}
function getLookupFromArray(array) {
const lookup = {};
array.forEach(itemId => {
lookup[itemId] = true;
});
return lookup;
}