@mui/x-tree-view 9.10.1 → 9.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +1 -11
  3. package/RichTreeView/RichTreeView.js +2 -0
  4. package/RichTreeView/RichTreeView.mjs +2 -0
  5. package/SimpleTreeView/SimpleTreeView.js +1 -0
  6. package/SimpleTreeView/SimpleTreeView.mjs +1 -0
  7. package/TreeItemIcon/TreeItemIcon.js +1 -0
  8. package/TreeItemIcon/TreeItemIcon.mjs +1 -0
  9. package/hooks/useTreeItemUtils/useTreeItemUtils.js +2 -0
  10. package/hooks/useTreeItemUtils/useTreeItemUtils.mjs +2 -0
  11. package/index.js +1 -1
  12. package/index.mjs +1 -1
  13. package/internals/MinimalTreeViewStore/MinimalTreeViewStore.d.mts +1 -0
  14. package/internals/MinimalTreeViewStore/MinimalTreeViewStore.d.ts +1 -0
  15. package/internals/RichTreeViewStore/RichTreeViewStore.d.mts +8 -2
  16. package/internals/RichTreeViewStore/RichTreeViewStore.d.ts +8 -2
  17. package/internals/RichTreeViewStore/RichTreeViewStore.js +5 -2
  18. package/internals/RichTreeViewStore/RichTreeViewStore.mjs +5 -2
  19. package/internals/plugins/items/TreeViewItemsPlugin.d.mts +27 -0
  20. package/internals/plugins/items/TreeViewItemsPlugin.d.ts +27 -0
  21. package/internals/plugins/items/TreeViewItemsPlugin.js +81 -35
  22. package/internals/plugins/items/TreeViewItemsPlugin.mjs +82 -36
  23. package/internals/plugins/items/utils.d.mts +28 -0
  24. package/internals/plugins/items/utils.d.ts +28 -0
  25. package/internals/plugins/items/utils.js +49 -0
  26. package/internals/plugins/items/utils.mjs +48 -0
  27. package/internals/plugins/selection/TreeViewSelectionPlugin.d.mts +15 -0
  28. package/internals/plugins/selection/TreeViewSelectionPlugin.d.ts +15 -0
  29. package/internals/plugins/selection/TreeViewSelectionPlugin.js +50 -10
  30. package/internals/plugins/selection/TreeViewSelectionPlugin.mjs +50 -10
  31. package/internals/plugins/selection/itemPlugin.js +3 -37
  32. package/internals/plugins/selection/itemPlugin.mjs +4 -38
  33. package/internals/plugins/selection/selectors.d.mts +11 -0
  34. package/internals/plugins/selection/selectors.d.ts +11 -0
  35. package/internals/plugins/selection/selectors.js +56 -3
  36. package/internals/plugins/selection/selectors.mjs +56 -3
  37. package/models/items.d.mts +2 -1
  38. package/models/items.d.ts +2 -1
  39. package/package.json +3 -3
  40. package/useTreeItem/useTreeItem.types.d.mts +4 -0
  41. package/useTreeItem/useTreeItem.types.d.ts +4 -0
@@ -1,7 +1,8 @@
1
+ import _formatErrorMessage from "@mui/x-internals/formatErrorMessage";
1
2
  import _extends from "@babel/runtime/helpers/esm/extends";
2
3
  import { idSelectors } from "../id/index.mjs";
3
4
  import { itemsSelectors } from "./selectors.mjs";
4
- import { buildItemsLookups, TREE_VIEW_ROOT_PARENT_ID } from "./utils.mjs";
5
+ import { buildItemsLookups, buildItemsLookupsRecursively, buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from "./utils.mjs";
5
6
  export class TreeViewItemsPlugin {
6
7
  // We can't type `store`, otherwise we get the following TS error:
7
8
  // 'items' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
@@ -23,41 +24,13 @@ export class TreeViewItemsPlugin {
23
24
  * Builds the state properties derived from the `items` prop.
24
25
  */
25
26
  static buildItemsStateIfNeeded = parameters => {
26
- const itemMetaLookup = {};
27
- const itemModelLookup = {};
28
- const itemOrderedChildrenIdsLookup = {};
29
- const itemChildrenIndexesLookup = {};
30
- function processSiblings(items, parentId, depth) {
31
- const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID;
32
- const {
33
- metaLookup,
34
- modelLookup,
35
- orderedChildrenIds,
36
- childrenIndexes,
37
- itemsChildren
38
- } = buildItemsLookups({
39
- storeParameters: parameters,
40
- items,
41
- parentId,
42
- depth,
43
- isItemExpandable: (item, children) => !!children && children.length > 0,
44
- otherItemsMetaLookup: itemMetaLookup
45
- });
46
- Object.assign(itemMetaLookup, metaLookup);
47
- Object.assign(itemModelLookup, modelLookup);
48
- itemOrderedChildrenIdsLookup[parentIdWithDefault] = orderedChildrenIds;
49
- itemChildrenIndexesLookup[parentIdWithDefault] = childrenIndexes;
50
- for (const item of itemsChildren) {
51
- processSiblings(item.children || [], item.id, depth + 1);
52
- }
53
- }
54
- processSiblings(parameters.items, null, 0);
55
- return {
56
- itemMetaLookup,
57
- itemModelLookup,
58
- itemOrderedChildrenIdsLookup,
59
- itemChildrenIndexesLookup
60
- };
27
+ return buildItemsLookupsRecursively({
28
+ storeParameters: parameters,
29
+ items: parameters.items,
30
+ parentId: null,
31
+ depth: 0,
32
+ isItemExpandable: (item, children) => !!children && children.length > 0
33
+ });
61
34
  };
62
35
 
63
36
  /**
@@ -124,6 +97,79 @@ export class TreeViewItemsPlugin {
124
97
  });
125
98
  this.store.set('itemMetaLookup', itemMetaLookup);
126
99
  };
100
+
101
+ /**
102
+ * Add items to the tree.
103
+ * 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.
104
+ * @param {AddItemsParameters<R>} parameters The items to add and their position in the tree.
105
+ */
106
+ addItems = ({
107
+ items,
108
+ parentId = null,
109
+ index
110
+ }) => {
111
+ if (items.length === 0) {
112
+ return;
113
+ }
114
+ if (parentId != null && itemsSelectors.itemMeta(this.store.state, parentId) == null) {
115
+ 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));
116
+ }
117
+ const parentDepth = parentId == null ? -1 : itemsSelectors.itemDepth(this.store.state, parentId);
118
+
119
+ // When the items are lazy loaded, an item can be expandable even if its children are not loaded yet.
120
+ const dataSource = this.store.parameters.dataSource;
121
+ const isItemExpandable = (item, children) => {
122
+ if (children != null && children.length > 0) {
123
+ return true;
124
+ }
125
+ return dataSource == null ? false : dataSource.getChildrenCount(item) !== 0;
126
+ };
127
+ const {
128
+ itemMetaLookup: metaLookup,
129
+ itemModelLookup: modelLookup,
130
+ itemOrderedChildrenIdsLookup: orderedChildrenIdsLookup,
131
+ itemChildrenIndexesLookup: childrenIndexesLookup
132
+ } = buildItemsLookupsRecursively({
133
+ storeParameters: this.store.parameters,
134
+ items,
135
+ parentId,
136
+ depth: parentDepth + 1,
137
+ isItemExpandable,
138
+ existingItemMetaLookup: this.store.state.itemMetaLookup
139
+ });
140
+
141
+ // `buildItemsLookups` allows an id to be re-used by an item with the same parent,
142
+ // which is only valid when rebuilding the state from the `items` prop.
143
+ for (const id of Object.keys(metaLookup)) {
144
+ if (this.store.state.itemMetaLookup[id] != null) {
145
+ 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));
146
+ }
147
+ }
148
+ const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID;
149
+ const existingChildrenIds = itemsSelectors.itemOrderedChildrenIds(this.store.state, parentId);
150
+ const insertionIndex = index ?? existingChildrenIds.length;
151
+ if (insertionIndex < 0 || insertionIndex > existingChildrenIds.length) {
152
+ 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));
153
+ }
154
+ const newChildrenIds = orderedChildrenIdsLookup[parentIdWithDefault];
155
+ const mergedChildrenIds = [...existingChildrenIds];
156
+ mergedChildrenIds.splice(insertionIndex, 0, ...newChildrenIds);
157
+ orderedChildrenIdsLookup[parentIdWithDefault] = mergedChildrenIds;
158
+ childrenIndexesLookup[parentIdWithDefault] = buildSiblingIndexes(mergedChildrenIds);
159
+ const newMetaLookup = _extends({}, this.store.state.itemMetaLookup, metaLookup);
160
+ if (parentId != null && !newMetaLookup[parentId].expandable) {
161
+ newMetaLookup[parentId] = _extends({}, newMetaLookup[parentId], {
162
+ expandable: true
163
+ });
164
+ }
165
+ this.store.update({
166
+ itemMetaLookup: newMetaLookup,
167
+ itemModelLookup: _extends({}, this.store.state.itemModelLookup, modelLookup),
168
+ itemOrderedChildrenIdsLookup: _extends({}, this.store.state.itemOrderedChildrenIdsLookup, orderedChildrenIdsLookup),
169
+ itemChildrenIndexesLookup: _extends({}, this.store.state.itemChildrenIndexesLookup, childrenIndexesLookup)
170
+ });
171
+ this.store.selection.propagateSelectionToNewItems(parentId, newChildrenIds);
172
+ };
127
173
  buildPublicAPI = () => {
128
174
  return {
129
175
  getItem: this.getItem,
@@ -30,6 +30,34 @@ export declare function buildItemsLookups<R extends TreeViewValidItem<R>>(parame
30
30
  children: R[];
31
31
  }[];
32
32
  };
33
+ /**
34
+ * Builds the items lookups for a tree of items, recursing into the children of each item.
35
+ * The returned lookups only contain the items passed to this method.
36
+ */
37
+ export declare function buildItemsLookupsRecursively<R extends TreeViewValidItem<R>>(parameters: BuildItemsLookupsRecursivelyParameters<R>): {
38
+ itemMetaLookup: {
39
+ [itemId: string]: TreeViewItemMeta;
40
+ };
41
+ itemModelLookup: {
42
+ [itemId: string]: R;
43
+ };
44
+ itemOrderedChildrenIdsLookup: {
45
+ [parentItemId: string]: string[];
46
+ };
47
+ itemChildrenIndexesLookup: {
48
+ [parentItemId: string]: {
49
+ [itemId: string]: number;
50
+ };
51
+ };
52
+ };
53
+ interface BuildItemsLookupsRecursivelyParameters<R extends TreeViewValidItem<R>> extends Omit<BuildItemsLookupsParameters<R>, 'otherItemsMetaLookup'> {
54
+ /**
55
+ * The meta of the items already present in the tree, used to detect duplicated ids.
56
+ */
57
+ existingItemMetaLookup?: {
58
+ [itemId: string]: TreeViewItemMeta;
59
+ };
60
+ }
33
61
  interface BuildItemsLookupsParameters<R extends TreeViewValidItem<R>> {
34
62
  items: readonly R[];
35
63
  storeParameters: Pick<MinimalTreeViewParameters<R, any>, 'getItemId' | 'getItemLabel' | 'getItemChildren' | 'isItemDisabled' | 'isItemSelectionDisabled'>;
@@ -30,6 +30,34 @@ export declare function buildItemsLookups<R extends TreeViewValidItem<R>>(parame
30
30
  children: R[];
31
31
  }[];
32
32
  };
33
+ /**
34
+ * Builds the items lookups for a tree of items, recursing into the children of each item.
35
+ * The returned lookups only contain the items passed to this method.
36
+ */
37
+ export declare function buildItemsLookupsRecursively<R extends TreeViewValidItem<R>>(parameters: BuildItemsLookupsRecursivelyParameters<R>): {
38
+ itemMetaLookup: {
39
+ [itemId: string]: TreeViewItemMeta;
40
+ };
41
+ itemModelLookup: {
42
+ [itemId: string]: R;
43
+ };
44
+ itemOrderedChildrenIdsLookup: {
45
+ [parentItemId: string]: string[];
46
+ };
47
+ itemChildrenIndexesLookup: {
48
+ [parentItemId: string]: {
49
+ [itemId: string]: number;
50
+ };
51
+ };
52
+ };
53
+ interface BuildItemsLookupsRecursivelyParameters<R extends TreeViewValidItem<R>> extends Omit<BuildItemsLookupsParameters<R>, 'otherItemsMetaLookup'> {
54
+ /**
55
+ * The meta of the items already present in the tree, used to detect duplicated ids.
56
+ */
57
+ existingItemMetaLookup?: {
58
+ [itemId: string]: TreeViewItemMeta;
59
+ };
60
+ }
33
61
  interface BuildItemsLookupsParameters<R extends TreeViewValidItem<R>> {
34
62
  items: readonly R[];
35
63
  storeParameters: Pick<MinimalTreeViewParameters<R, any>, 'getItemId' | 'getItemLabel' | 'getItemChildren' | 'isItemDisabled' | 'isItemSelectionDisabled'>;
@@ -6,7 +6,9 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.TREE_VIEW_ROOT_PARENT_ID = void 0;
8
8
  exports.buildItemsLookups = buildItemsLookups;
9
+ exports.buildItemsLookupsRecursively = buildItemsLookupsRecursively;
9
10
  exports.isItemDisabled = exports.buildSiblingIndexes = void 0;
11
+ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
10
12
  var _formatErrorMessage2 = _interopRequireDefault(require("@mui/x-internals/formatErrorMessage"));
11
13
  const TREE_VIEW_ROOT_PARENT_ID = exports.TREE_VIEW_ROOT_PARENT_ID = '__TREE_VIEW_ROOT_PARENT_ID__';
12
14
  const buildSiblingIndexes = siblings => {
@@ -104,6 +106,53 @@ function buildItemsLookups(parameters) {
104
106
  itemsChildren
105
107
  };
106
108
  }
109
+
110
+ /**
111
+ * Builds the items lookups for a tree of items, recursing into the children of each item.
112
+ * The returned lookups only contain the items passed to this method.
113
+ */
114
+ function buildItemsLookupsRecursively(parameters) {
115
+ const {
116
+ storeParameters,
117
+ items,
118
+ parentId,
119
+ depth,
120
+ isItemExpandable,
121
+ existingItemMetaLookup
122
+ } = parameters;
123
+ const itemMetaLookup = {};
124
+ const itemModelLookup = {};
125
+ const itemOrderedChildrenIdsLookup = {};
126
+ const itemChildrenIndexesLookup = {};
127
+
128
+ // Both the existing items and the ones processed here, to detect duplicated ids.
129
+ const otherItemsMetaLookup = (0, _extends2.default)({}, existingItemMetaLookup);
130
+ const processSiblings = (siblings, siblingsParentId, siblingsDepth) => {
131
+ const lookups = buildItemsLookups({
132
+ storeParameters,
133
+ items: siblings,
134
+ parentId: siblingsParentId,
135
+ depth: siblingsDepth,
136
+ isItemExpandable,
137
+ otherItemsMetaLookup
138
+ });
139
+ Object.assign(itemMetaLookup, lookups.metaLookup);
140
+ Object.assign(otherItemsMetaLookup, lookups.metaLookup);
141
+ Object.assign(itemModelLookup, lookups.modelLookup);
142
+ itemOrderedChildrenIdsLookup[siblingsParentId ?? TREE_VIEW_ROOT_PARENT_ID] = lookups.orderedChildrenIds;
143
+ itemChildrenIndexesLookup[siblingsParentId ?? TREE_VIEW_ROOT_PARENT_ID] = lookups.childrenIndexes;
144
+ for (const item of lookups.itemsChildren) {
145
+ processSiblings(item.children, item.id, siblingsDepth + 1);
146
+ }
147
+ };
148
+ processSiblings(items, parentId, depth);
149
+ return {
150
+ itemMetaLookup,
151
+ itemModelLookup,
152
+ itemOrderedChildrenIdsLookup,
153
+ itemChildrenIndexesLookup
154
+ };
155
+ }
107
156
  function checkId({
108
157
  id,
109
158
  parentId,
@@ -1,3 +1,4 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
1
2
  import _formatErrorMessage from "@mui/x-internals/formatErrorMessage";
2
3
  export const TREE_VIEW_ROOT_PARENT_ID = '__TREE_VIEW_ROOT_PARENT_ID__';
3
4
  export const buildSiblingIndexes = siblings => {
@@ -93,6 +94,53 @@ export function buildItemsLookups(parameters) {
93
94
  itemsChildren
94
95
  };
95
96
  }
97
+
98
+ /**
99
+ * Builds the items lookups for a tree of items, recursing into the children of each item.
100
+ * The returned lookups only contain the items passed to this method.
101
+ */
102
+ export function buildItemsLookupsRecursively(parameters) {
103
+ const {
104
+ storeParameters,
105
+ items,
106
+ parentId,
107
+ depth,
108
+ isItemExpandable,
109
+ existingItemMetaLookup
110
+ } = parameters;
111
+ const itemMetaLookup = {};
112
+ const itemModelLookup = {};
113
+ const itemOrderedChildrenIdsLookup = {};
114
+ const itemChildrenIndexesLookup = {};
115
+
116
+ // Both the existing items and the ones processed here, to detect duplicated ids.
117
+ const otherItemsMetaLookup = _extends({}, existingItemMetaLookup);
118
+ const processSiblings = (siblings, siblingsParentId, siblingsDepth) => {
119
+ const lookups = buildItemsLookups({
120
+ storeParameters,
121
+ items: siblings,
122
+ parentId: siblingsParentId,
123
+ depth: siblingsDepth,
124
+ isItemExpandable,
125
+ otherItemsMetaLookup
126
+ });
127
+ Object.assign(itemMetaLookup, lookups.metaLookup);
128
+ Object.assign(otherItemsMetaLookup, lookups.metaLookup);
129
+ Object.assign(itemModelLookup, lookups.modelLookup);
130
+ itemOrderedChildrenIdsLookup[siblingsParentId ?? TREE_VIEW_ROOT_PARENT_ID] = lookups.orderedChildrenIds;
131
+ itemChildrenIndexesLookup[siblingsParentId ?? TREE_VIEW_ROOT_PARENT_ID] = lookups.childrenIndexes;
132
+ for (const item of lookups.itemsChildren) {
133
+ processSiblings(item.children, item.id, siblingsDepth + 1);
134
+ }
135
+ };
136
+ processSiblings(items, parentId, depth);
137
+ return {
138
+ itemMetaLookup,
139
+ itemModelLookup,
140
+ itemOrderedChildrenIdsLookup,
141
+ itemChildrenIndexesLookup
142
+ };
143
+ }
96
144
  function checkId({
97
145
  id,
98
146
  parentId,
@@ -1,3 +1,4 @@
1
+ import type { TreeViewItemId, TreeViewItemSelectionStatus } from "../../../models/index.mjs";
1
2
  export declare class TreeViewSelectionPlugin<Multiple extends boolean | undefined> {
2
3
  private store;
3
4
  private lastSelectedItem;
@@ -5,7 +6,15 @@ export declare class TreeViewSelectionPlugin<Multiple extends boolean | undefine
5
6
  constructor(store: any);
6
7
  private setSelectedItems;
7
8
  private selectRange;
9
+ /**
10
+ * Get the selection status of an item.
11
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
12
+ * @param {TreeViewItemId} itemId The id of the item to get the selection status of.
13
+ * @returns {TreeViewItemSelectionStatus} The selection status of the item.
14
+ */
15
+ private getItemSelection;
8
16
  buildPublicAPI: () => {
17
+ getItemSelection: (itemId: TreeViewItemId) => TreeViewItemSelectionStatus;
9
18
  setItemSelection: ({
10
19
  itemId,
11
20
  event,
@@ -18,6 +27,12 @@ export declare class TreeViewSelectionPlugin<Multiple extends boolean | undefine
18
27
  keepExistingSelection?: boolean;
19
28
  }) => void;
20
29
  };
30
+ /**
31
+ * Select the items added below a selected parent when the selection propagates to the descendants.
32
+ * @param {TreeViewItemId | null} parentId The id of the item the new items were added to.
33
+ * @param {TreeViewItemId[]} newItemIds The ids of the items that were just added.
34
+ */
35
+ propagateSelectionToNewItems: (parentId: TreeViewItemId | null, newItemIds: TreeViewItemId[]) => void;
21
36
  /**
22
37
  * Select or deselect an item.
23
38
  * @param {object} parameters The parameters of the method.
@@ -1,3 +1,4 @@
1
+ import type { TreeViewItemId, TreeViewItemSelectionStatus } from "../../../models/index.js";
1
2
  export declare class TreeViewSelectionPlugin<Multiple extends boolean | undefined> {
2
3
  private store;
3
4
  private lastSelectedItem;
@@ -5,7 +6,15 @@ export declare class TreeViewSelectionPlugin<Multiple extends boolean | undefine
5
6
  constructor(store: any);
6
7
  private setSelectedItems;
7
8
  private selectRange;
9
+ /**
10
+ * Get the selection status of an item.
11
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
12
+ * @param {TreeViewItemId} itemId The id of the item to get the selection status of.
13
+ * @returns {TreeViewItemSelectionStatus} The selection status of the item.
14
+ */
15
+ private getItemSelection;
8
16
  buildPublicAPI: () => {
17
+ getItemSelection: (itemId: TreeViewItemId) => TreeViewItemSelectionStatus;
9
18
  setItemSelection: ({
10
19
  itemId,
11
20
  event,
@@ -18,6 +27,12 @@ export declare class TreeViewSelectionPlugin<Multiple extends boolean | undefine
18
27
  keepExistingSelection?: boolean;
19
28
  }) => void;
20
29
  };
30
+ /**
31
+ * Select the items added below a selected parent when the selection propagates to the descendants.
32
+ * @param {TreeViewItemId | null} parentId The id of the item the new items were added to.
33
+ * @param {TreeViewItemId[]} newItemIds The ids of the items that were just added.
34
+ */
35
+ propagateSelectionToNewItems: (parentId: TreeViewItemId | null, newItemIds: TreeViewItemId[]) => void;
21
36
  /**
22
37
  * Select or deselect an item.
23
38
  * @param {object} parameters The parameters of the method.
@@ -41,10 +41,16 @@ class TreeViewSelectionPlugin {
41
41
  } else {
42
42
  cleanModel = newModel;
43
43
  }
44
+
45
+ // The store is updated before the callbacks are fired,
46
+ // so that the selection selectors and the `getItemSelection` API method
47
+ // return the new selection status when called from `onItemSelectionToggle` or `onSelectedItemsChange`.
48
+ if (selectedItems === undefined) {
49
+ this.store.set('selectedItems', cleanModel);
50
+ }
44
51
  if (onItemSelectionToggle) {
45
52
  if (isMultiSelectEnabled) {
46
53
  const changes = getAddedAndRemovedItems({
47
- store: this.store,
48
54
  newModel: cleanModel,
49
55
  oldModel: oldModel
50
56
  });
@@ -65,9 +71,6 @@ class TreeViewSelectionPlugin {
65
71
  }
66
72
  }
67
73
  }
68
- if (selectedItems === undefined) {
69
- this.store.set('selectedItems', cleanModel);
70
- }
71
74
  onSelectedItemsChange?.(event, cleanModel);
72
75
  };
73
76
  selectRange = (event, [start, end]) => {
@@ -91,12 +94,39 @@ class TreeViewSelectionPlugin {
91
94
  this.setSelectedItems(event, newSelectedItems);
92
95
  this.lastSelectedRange = getLookupFromArray(range);
93
96
  };
97
+
98
+ /**
99
+ * Get the selection status of an item.
100
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
101
+ * @param {TreeViewItemId} itemId The id of the item to get the selection status of.
102
+ * @returns {TreeViewItemSelectionStatus} The selection status of the item.
103
+ */
104
+ getItemSelection = itemId => _selectors.selectionSelectors.itemSelectionStatus(this.store.state, itemId);
94
105
  buildPublicAPI = () => {
95
106
  return {
107
+ getItemSelection: this.getItemSelection,
96
108
  setItemSelection: this.setItemSelection
97
109
  };
98
110
  };
99
111
 
112
+ /**
113
+ * Select the items added below a selected parent when the selection propagates to the descendants.
114
+ * @param {TreeViewItemId | null} parentId The id of the item the new items were added to.
115
+ * @param {TreeViewItemId[]} newItemIds The ids of the items that were just added.
116
+ */
117
+ propagateSelectionToNewItems = (parentId, newItemIds) => {
118
+ const {
119
+ selectionPropagation = _empty.EMPTY_OBJECT
120
+ } = this.store.parameters;
121
+ if (parentId == null || newItemIds.length === 0 || !selectionPropagation.descendants || !_selectors.selectionSelectors.isMultiSelectEnabled(this.store.state) || !_selectors.selectionSelectors.isItemSelected(this.store.state, parentId)) {
122
+ return;
123
+ }
124
+
125
+ // Only propagate to the new items, the rest of the parent's subtree is already up to date.
126
+ const newModel = _selectors.selectionSelectors.selectedItems(this.store.state).concat(newItemIds);
127
+ this.setSelectedItems(null, newModel, newItemIds);
128
+ };
129
+
100
130
  /**
101
131
  * Select or deselect an item.
102
132
  * @param {object} parameters The parameters of the method.
@@ -116,7 +146,7 @@ class TreeViewSelectionPlugin {
116
146
  }
117
147
  let newSelected;
118
148
  const isMultiSelectEnabled = _selectors.selectionSelectors.isMultiSelectEnabled(this.store.state);
119
- if (keepExistingSelection) {
149
+ if (keepExistingSelection && isMultiSelectEnabled) {
120
150
  const oldSelected = _selectors.selectionSelectors.selectedItems(this.store.state);
121
151
  const isSelectedBefore = _selectors.selectionSelectors.isItemSelected(this.store.state, itemId);
122
152
  if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) {
@@ -199,7 +229,9 @@ class TreeViewSelectionPlugin {
199
229
  }
200
230
  let newSelectedItems = _selectors.selectionSelectors.selectedItems(this.store.state).slice();
201
231
  if (Object.keys(this.lastSelectedRange).length === 0) {
202
- newSelectedItems.push(nextItem);
232
+ if (!_selectors.selectionSelectors.isItemSelected(this.store.state, nextItem)) {
233
+ newSelectedItems.push(nextItem);
234
+ }
203
235
  this.lastSelectedRange = {
204
236
  [currentItem]: true,
205
237
  [nextItem]: true
@@ -212,7 +244,9 @@ class TreeViewSelectionPlugin {
212
244
  newSelectedItems = newSelectedItems.filter(id => id !== currentItem);
213
245
  delete this.lastSelectedRange[currentItem];
214
246
  } else {
215
- newSelectedItems.push(nextItem);
247
+ if (!_selectors.selectionSelectors.isItemSelected(this.store.state, nextItem)) {
248
+ newSelectedItems.push(nextItem);
249
+ }
216
250
  this.lastSelectedRange[nextItem] = true;
217
251
  }
218
252
  }
@@ -233,7 +267,6 @@ function propagateSelection({
233
267
  let shouldRegenerateModel = false;
234
268
  const newModelLookup = getLookupFromArray(newModel);
235
269
  const changes = getAddedAndRemovedItems({
236
- store,
237
270
  newModel,
238
271
  oldModel
239
272
  });
@@ -313,8 +346,11 @@ function propagateSelection({
313
346
  });
314
347
  return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel;
315
348
  }
349
+
350
+ // This method only diffs the two models it receives,
351
+ // it must not read the selection from the store,
352
+ // otherwise the result would depend on whether the store has already been updated or not.
316
353
  function getAddedAndRemovedItems({
317
- store,
318
354
  oldModel,
319
355
  newModel
320
356
  }) {
@@ -322,8 +358,12 @@ function getAddedAndRemovedItems({
322
358
  newModel.forEach(id => {
323
359
  newModelMap.set(id, true);
324
360
  });
361
+ const oldModelMap = new Map();
362
+ oldModel.forEach(id => {
363
+ oldModelMap.set(id, true);
364
+ });
325
365
  return {
326
- added: newModel.filter(itemId => !_selectors.selectionSelectors.isItemSelected(store.state, itemId)),
366
+ added: newModel.filter(itemId => !oldModelMap.has(itemId)),
327
367
  removed: oldModel.filter(itemId => !newModelMap.has(itemId))
328
368
  };
329
369
  }
@@ -34,10 +34,16 @@ export class TreeViewSelectionPlugin {
34
34
  } else {
35
35
  cleanModel = newModel;
36
36
  }
37
+
38
+ // The store is updated before the callbacks are fired,
39
+ // so that the selection selectors and the `getItemSelection` API method
40
+ // return the new selection status when called from `onItemSelectionToggle` or `onSelectedItemsChange`.
41
+ if (selectedItems === undefined) {
42
+ this.store.set('selectedItems', cleanModel);
43
+ }
37
44
  if (onItemSelectionToggle) {
38
45
  if (isMultiSelectEnabled) {
39
46
  const changes = getAddedAndRemovedItems({
40
- store: this.store,
41
47
  newModel: cleanModel,
42
48
  oldModel: oldModel
43
49
  });
@@ -58,9 +64,6 @@ export class TreeViewSelectionPlugin {
58
64
  }
59
65
  }
60
66
  }
61
- if (selectedItems === undefined) {
62
- this.store.set('selectedItems', cleanModel);
63
- }
64
67
  onSelectedItemsChange?.(event, cleanModel);
65
68
  };
66
69
  selectRange = (event, [start, end]) => {
@@ -84,12 +87,39 @@ export class TreeViewSelectionPlugin {
84
87
  this.setSelectedItems(event, newSelectedItems);
85
88
  this.lastSelectedRange = getLookupFromArray(range);
86
89
  };
90
+
91
+ /**
92
+ * Get the selection status of an item.
93
+ * An item that is not selected is `indeterminate` when some of its selectable descendants are selected.
94
+ * @param {TreeViewItemId} itemId The id of the item to get the selection status of.
95
+ * @returns {TreeViewItemSelectionStatus} The selection status of the item.
96
+ */
97
+ getItemSelection = itemId => selectionSelectors.itemSelectionStatus(this.store.state, itemId);
87
98
  buildPublicAPI = () => {
88
99
  return {
100
+ getItemSelection: this.getItemSelection,
89
101
  setItemSelection: this.setItemSelection
90
102
  };
91
103
  };
92
104
 
105
+ /**
106
+ * Select the items added below a selected parent when the selection propagates to the descendants.
107
+ * @param {TreeViewItemId | null} parentId The id of the item the new items were added to.
108
+ * @param {TreeViewItemId[]} newItemIds The ids of the items that were just added.
109
+ */
110
+ propagateSelectionToNewItems = (parentId, newItemIds) => {
111
+ const {
112
+ selectionPropagation = EMPTY_OBJECT
113
+ } = this.store.parameters;
114
+ if (parentId == null || newItemIds.length === 0 || !selectionPropagation.descendants || !selectionSelectors.isMultiSelectEnabled(this.store.state) || !selectionSelectors.isItemSelected(this.store.state, parentId)) {
115
+ return;
116
+ }
117
+
118
+ // Only propagate to the new items, the rest of the parent's subtree is already up to date.
119
+ const newModel = selectionSelectors.selectedItems(this.store.state).concat(newItemIds);
120
+ this.setSelectedItems(null, newModel, newItemIds);
121
+ };
122
+
93
123
  /**
94
124
  * Select or deselect an item.
95
125
  * @param {object} parameters The parameters of the method.
@@ -109,7 +139,7 @@ export class TreeViewSelectionPlugin {
109
139
  }
110
140
  let newSelected;
111
141
  const isMultiSelectEnabled = selectionSelectors.isMultiSelectEnabled(this.store.state);
112
- if (keepExistingSelection) {
142
+ if (keepExistingSelection && isMultiSelectEnabled) {
113
143
  const oldSelected = selectionSelectors.selectedItems(this.store.state);
114
144
  const isSelectedBefore = selectionSelectors.isItemSelected(this.store.state, itemId);
115
145
  if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) {
@@ -192,7 +222,9 @@ export class TreeViewSelectionPlugin {
192
222
  }
193
223
  let newSelectedItems = selectionSelectors.selectedItems(this.store.state).slice();
194
224
  if (Object.keys(this.lastSelectedRange).length === 0) {
195
- newSelectedItems.push(nextItem);
225
+ if (!selectionSelectors.isItemSelected(this.store.state, nextItem)) {
226
+ newSelectedItems.push(nextItem);
227
+ }
196
228
  this.lastSelectedRange = {
197
229
  [currentItem]: true,
198
230
  [nextItem]: true
@@ -205,7 +237,9 @@ export class TreeViewSelectionPlugin {
205
237
  newSelectedItems = newSelectedItems.filter(id => id !== currentItem);
206
238
  delete this.lastSelectedRange[currentItem];
207
239
  } else {
208
- newSelectedItems.push(nextItem);
240
+ if (!selectionSelectors.isItemSelected(this.store.state, nextItem)) {
241
+ newSelectedItems.push(nextItem);
242
+ }
209
243
  this.lastSelectedRange[nextItem] = true;
210
244
  }
211
245
  }
@@ -225,7 +259,6 @@ function propagateSelection({
225
259
  let shouldRegenerateModel = false;
226
260
  const newModelLookup = getLookupFromArray(newModel);
227
261
  const changes = getAddedAndRemovedItems({
228
- store,
229
262
  newModel,
230
263
  oldModel
231
264
  });
@@ -305,8 +338,11 @@ function propagateSelection({
305
338
  });
306
339
  return shouldRegenerateModel ? Object.keys(newModelLookup) : newModel;
307
340
  }
341
+
342
+ // This method only diffs the two models it receives,
343
+ // it must not read the selection from the store,
344
+ // otherwise the result would depend on whether the store has already been updated or not.
308
345
  function getAddedAndRemovedItems({
309
- store,
310
346
  oldModel,
311
347
  newModel
312
348
  }) {
@@ -314,8 +350,12 @@ function getAddedAndRemovedItems({
314
350
  newModel.forEach(id => {
315
351
  newModelMap.set(id, true);
316
352
  });
353
+ const oldModelMap = new Map();
354
+ oldModel.forEach(id => {
355
+ oldModelMap.set(id, true);
356
+ });
317
357
  return {
318
- added: newModel.filter(itemId => !selectionSelectors.isItemSelected(store.state, itemId)),
358
+ added: newModel.filter(itemId => !oldModelMap.has(itemId)),
319
359
  removed: oldModel.filter(itemId => !newModelMap.has(itemId))
320
360
  };
321
361
  }