@mui/x-tree-view
Version:
The community edition of the MUI X Tree View components.
310 lines (294 loc) • 13.4 kB
JavaScript
import _formatErrorMessage from "@mui/x-internals/formatErrorMessage";
import _extends from "@babel/runtime/helpers/esm/extends";
import { idSelectors } from "../id/index.mjs";
import { itemsSelectors } from "./selectors.mjs";
import { buildItemsLookups, buildItemsLookupsRecursively, buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from "./utils.mjs";
export class TreeViewItemsPlugin {
// We can't type `store`, otherwise we get the following TS error:
// 'items' 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;
}
/**
* Determines if the items state should be rebuilt based on the new and previous parameters.
*/
static shouldRebuildItemsState = (newParameters, previousParameters) => {
return ['items', 'isItemDisabled', 'isItemSelectionDisabled', 'getItemId', 'getItemLabel', 'getItemChildren'].some(key => {
const typedKey = key;
return newParameters[typedKey] !== previousParameters[typedKey];
});
};
/**
* Builds the state properties derived from the `items` prop.
*/
static buildItemsStateIfNeeded = parameters => {
return buildItemsLookupsRecursively({
storeParameters: parameters,
items: parameters.items,
parentId: null,
depth: 0,
isItemExpandable: (item, children) => !!children && children.length > 0
});
};
/**
* Get the item with the given id.
* When used in the Simple Tree View, it returns an object with the `id` and `label` properties.
* @param {TreeViewItemId} itemId The id of the item to retrieve.
* @returns {R} The item with the given id.
*/
getItem = itemId => itemsSelectors.itemModel(this.store.state, itemId);
/**
* Get all the items in the same format as provided by `props.items`.
* @returns {R[]} The items in the tree.
*/
getItemTree = () => {
const getItemFromItemId = itemId => {
const item = itemsSelectors.itemModel(this.store.state, itemId);
const itemToMutate = _extends({}, item);
const newChildren = itemsSelectors.itemOrderedChildrenIds(this.store.state, itemId);
if (newChildren.length > 0) {
itemToMutate.children = newChildren.map(getItemFromItemId);
} else {
delete itemToMutate.children;
}
return itemToMutate;
};
return itemsSelectors.itemOrderedChildrenIds(this.store.state, null).map(getItemFromItemId);
};
/**
* Get the ids of a given item's children.
* Those ids are returned in the order they should be rendered.
* To get the root items, pass `null` as the `itemId`.
* @param {TreeViewItemId | null} itemId The id of the item to get the children of.
* @returns {TreeViewItemId[]} The ids of the item's children.
*/
getItemOrderedChildrenIds = itemId => itemsSelectors.itemOrderedChildrenIds(this.store.state, itemId);
/** * Get the id of the parent item.
* @param {TreeViewItemId} itemId The id of the item to whose parentId we want to retrieve.
* @returns {TreeViewItemId | null} The id of the parent item.
*/
getParentId = itemId => {
const itemMeta = itemsSelectors.itemMeta(this.store.state, itemId);
return itemMeta?.parentId || null;
};
/**
* Toggle the disabled state of the item with the given id.
* @param {object} parameters The params of the method.
* @param {TreeViewItemId } parameters.itemId The id of the item to get the children of.
* @param {boolean } parameters.shouldBeDisabled true if the item should be disabled.
*/
setIsItemDisabled = ({
itemId,
shouldBeDisabled
}) => {
if (!this.store.state.itemMetaLookup[itemId]) {
return;
}
const itemMetaLookup = _extends({}, this.store.state.itemMetaLookup);
itemMetaLookup[itemId] = _extends({}, itemMetaLookup[itemId], {
disabled: shouldBeDisabled ?? !itemMetaLookup[itemId].disabled
});
this.store.set('itemMetaLookup', itemMetaLookup);
};
/**
* Add items to the tree.
* The items are added as children of the item with the given `parentId`, or at the root level if `parentId` is `null` or not defined.
* @param {AddItemsParameters<R>} parameters The items to add and their position in the tree.
*/
addItems = ({
items,
parentId = null,
index
}) => {
if (items.length === 0) {
return;
}
if (parentId != null && itemsSelectors.itemMeta(this.store.state, parentId) == null) {
throw new Error(process.env.NODE_ENV !== "production" ? `MUI X Tree View: Unable to add items to the parent with id "${parentId}" because it is not present in the tree. ` + 'Pass the id of an existing item, or `null` to add the items at the root level.' : _formatErrorMessage(295, parentId));
}
const parentDepth = parentId == null ? -1 : itemsSelectors.itemDepth(this.store.state, parentId);
// When the items are lazy loaded, an item can be expandable even if its children are not loaded yet.
const dataSource = this.store.parameters.dataSource;
const isItemExpandable = (item, children) => {
if (children != null && children.length > 0) {
return true;
}
return dataSource == null ? false : dataSource.getChildrenCount(item) !== 0;
};
const {
itemMetaLookup: metaLookup,
itemModelLookup: modelLookup,
itemOrderedChildrenIdsLookup: orderedChildrenIdsLookup,
itemChildrenIndexesLookup: childrenIndexesLookup
} = buildItemsLookupsRecursively({
storeParameters: this.store.parameters,
items,
parentId,
depth: parentDepth + 1,
isItemExpandable,
existingItemMetaLookup: this.store.state.itemMetaLookup
});
// `buildItemsLookups` allows an id to be re-used by an item with the same parent,
// which is only valid when rebuilding the state from the `items` prop.
for (const id of Object.keys(metaLookup)) {
if (this.store.state.itemMetaLookup[id] != null) {
throw new Error(process.env.NODE_ENV !== "production" ? `MUI X Tree View: All items must have a unique \`id\` property. ` + `The id "${id}" is used by multiple items. ` + 'Use the `getItemId` prop to specify a custom id for each item if needed.' : _formatErrorMessage(194, id));
}
}
const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID;
const existingChildrenIds = itemsSelectors.itemOrderedChildrenIds(this.store.state, parentId);
const insertionIndex = index ?? existingChildrenIds.length;
if (insertionIndex < 0 || insertionIndex > existingChildrenIds.length) {
throw new Error(process.env.NODE_ENV !== "production" ? `MUI X Tree View: Unable to add items at index "${insertionIndex}" because it is out of range. ` + `The index must be between 0 and the amount of children of the parent item (${existingChildrenIds.length}).` : _formatErrorMessage(296, insertionIndex, existingChildrenIds.length));
}
const newChildrenIds = orderedChildrenIdsLookup[parentIdWithDefault];
const mergedChildrenIds = [...existingChildrenIds];
mergedChildrenIds.splice(insertionIndex, 0, ...newChildrenIds);
orderedChildrenIdsLookup[parentIdWithDefault] = mergedChildrenIds;
childrenIndexesLookup[parentIdWithDefault] = buildSiblingIndexes(mergedChildrenIds);
const newMetaLookup = _extends({}, this.store.state.itemMetaLookup, metaLookup);
if (parentId != null && !newMetaLookup[parentId].expandable) {
newMetaLookup[parentId] = _extends({}, newMetaLookup[parentId], {
expandable: true
});
}
this.store.update({
itemMetaLookup: newMetaLookup,
itemModelLookup: _extends({}, this.store.state.itemModelLookup, modelLookup),
itemOrderedChildrenIdsLookup: _extends({}, this.store.state.itemOrderedChildrenIdsLookup, orderedChildrenIdsLookup),
itemChildrenIndexesLookup: _extends({}, this.store.state.itemChildrenIndexesLookup, childrenIndexesLookup)
});
this.store.selection.propagateSelectionToNewItems(parentId, newChildrenIds);
};
buildPublicAPI = () => {
return {
getItem: this.getItem,
getItemDOMElement: this.getItemDOMElement,
getItemOrderedChildrenIds: this.getItemOrderedChildrenIds,
getItemTree: this.getItemTree,
getParentId: this.getParentId,
setIsItemDisabled: this.setIsItemDisabled
};
};
/**
* Get the DOM element of the item with the given id.
* @param {TreeViewItemId} itemId The id of the item to get the DOM element of.
* @returns {HTMLElement | null} The DOM element of the item with the given id.
*/
getItemDOMElement = itemId => {
const itemMeta = itemsSelectors.itemMeta(this.store.state, itemId);
if (itemMeta == null) {
return null;
}
const idAttribute = idSelectors.treeItemIdAttribute(this.store.state, itemId, itemMeta.idAttribute);
return document.getElementById(idAttribute);
};
/**
* Set the children of an item.
* @param {SetItemChildrenParameters<R>} parameters The children to set and information about their parent.
*/
setItemChildren = ({
items,
parentId,
getChildrenCount,
recursive = false
}) => {
const parentDepth = parentId == null ? -1 : itemsSelectors.itemDepth(this.store.state, parentId);
const existingItemMetaLookup = itemsSelectors.itemMetaLookup(this.store.state);
const buildParameters = {
storeParameters: this.store.parameters,
items,
parentId,
depth: parentDepth + 1,
isItemExpandable: getChildrenCount ? item => getChildrenCount(item) !== 0 : () => false
};
let lookups;
if (recursive) {
lookups = buildItemsLookupsRecursively(_extends({}, buildParameters, {
existingItemMetaLookup
}));
} else {
const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID;
const {
metaLookup,
modelLookup,
orderedChildrenIds,
childrenIndexes
} = buildItemsLookups(_extends({}, buildParameters, {
otherItemsMetaLookup: existingItemMetaLookup
}));
lookups = {
itemMetaLookup: metaLookup,
itemModelLookup: modelLookup,
itemOrderedChildrenIdsLookup: {
[parentIdWithDefault]: orderedChildrenIds
},
itemChildrenIndexesLookup: {
[parentIdWithDefault]: childrenIndexes
}
};
}
const itemModelLookup = _extends({}, this.store.state.itemModelLookup);
const itemMetaLookup = _extends({}, this.store.state.itemMetaLookup);
const itemOrderedChildrenIdsLookup = _extends({}, this.store.state.itemOrderedChildrenIdsLookup);
const itemChildrenIndexesLookup = _extends({}, this.store.state.itemChildrenIndexesLookup);
// Remove the previous children that are not part of the new ones (and their descendants),
// otherwise they could not be added back under another parent (for example after a move).
const removeItem = itemId => {
if (lookups.itemMetaLookup[itemId] != null) {
return;
}
for (const childId of itemOrderedChildrenIdsLookup[itemId] ?? []) {
removeItem(childId);
}
delete itemModelLookup[itemId];
delete itemMetaLookup[itemId];
delete itemOrderedChildrenIdsLookup[itemId];
delete itemChildrenIndexesLookup[itemId];
};
for (const replacedParentId of Object.keys(lookups.itemOrderedChildrenIdsLookup)) {
for (const childId of itemOrderedChildrenIdsLookup[replacedParentId] ?? []) {
removeItem(childId);
}
}
// A single update, so the listeners are notified once no matter how many groups of items were added.
this.store.update({
itemModelLookup: _extends({}, itemModelLookup, lookups.itemModelLookup),
itemMetaLookup: _extends({}, itemMetaLookup, lookups.itemMetaLookup),
itemOrderedChildrenIdsLookup: _extends({}, itemOrderedChildrenIdsLookup, lookups.itemOrderedChildrenIdsLookup),
itemChildrenIndexesLookup: _extends({}, itemChildrenIndexesLookup, lookups.itemChildrenIndexesLookup)
});
};
/**
* Remove the children of an item.
* @param {TreeViewItemId | null} parentId The id of the item to remove the children of.
*/
removeChildren = parentId => {
const itemMetaLookup = this.store.state.itemMetaLookup;
const newItemMetaLookup = {};
for (const itemId of Object.keys(itemMetaLookup)) {
const itemMeta = itemMetaLookup[itemId];
if (itemMeta.parentId !== parentId) {
newItemMetaLookup[itemId] = itemMeta;
}
}
const newItemOrderedChildrenIdsLookup = _extends({}, this.store.state.itemOrderedChildrenIdsLookup);
const newItemChildrenIndexesLookup = _extends({}, this.store.state.itemChildrenIndexesLookup);
const cleanId = parentId ?? TREE_VIEW_ROOT_PARENT_ID;
delete newItemChildrenIndexesLookup[cleanId];
delete newItemOrderedChildrenIdsLookup[cleanId];
this.store.update({
itemMetaLookup: newItemMetaLookup,
itemOrderedChildrenIdsLookup: newItemOrderedChildrenIdsLookup,
itemChildrenIndexesLookup: newItemChildrenIndexesLookup
});
};
/**
* Callback fired when the `content` slot of a given Tree Item is clicked.
* @param {React.MouseEvent} event The DOM event that triggered the change.
* @param {TreeViewItemId} itemId The id of the item being clicked.
*/
handleItemClick = (event, itemId) => {
this.store.parameters.onItemClick?.(event, itemId);
};
}