UNPKG

@mui/x-tree-view

Version:

The community edition of the MUI X Tree View components.

327 lines (317 loc) 12.7 kB
"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; }