@mui/x-tree-view 7.3.0 → 7.3.1
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.
- package/CHANGELOG.md +79 -2
- package/TreeItem/TreeItem.js +1 -2
- package/TreeItem/TreeItemContent.js +1 -2
- package/TreeItem/useTreeItemState.js +1 -3
- package/TreeItem2/TreeItem2.js +1 -2
- package/hooks/useTreeItem2Utils/useTreeItem2Utils.js +1 -3
- package/index.js +1 -1
- package/internals/TreeViewProvider/useTreeViewContext.d.ts +1 -1
- package/internals/hooks/useInstanceEventHandler.d.ts +2 -2
- package/internals/models/treeView.d.ts +0 -6
- package/internals/plugins/useTreeViewFocus/useTreeViewFocus.js +1 -4
- package/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.js +31 -63
- package/internals/plugins/useTreeViewSelection/useTreeViewSelection.js +82 -94
- package/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.d.ts +32 -5
- package/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.d.ts +9 -0
- package/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.js +21 -0
- package/internals/useTreeView/useTreeView.types.d.ts +1 -1
- package/internals/useTreeView/useTreeViewModels.d.ts +1 -1
- package/internals/utils/extractPluginParamsFromProps.d.ts +1 -1
- package/internals/utils/publishTreeViewEvent.d.ts +1 -1
- package/internals/utils/tree.d.ts +17 -1
- package/internals/utils/tree.js +34 -4
- package/modern/TreeItem/TreeItem.js +1 -2
- package/modern/TreeItem/TreeItemContent.js +1 -2
- package/modern/TreeItem/useTreeItemState.js +1 -3
- package/modern/TreeItem2/TreeItem2.js +1 -2
- package/modern/hooks/useTreeItem2Utils/useTreeItem2Utils.js +1 -3
- package/modern/index.js +1 -1
- package/modern/internals/plugins/useTreeViewFocus/useTreeViewFocus.js +1 -4
- package/modern/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.js +31 -63
- package/modern/internals/plugins/useTreeViewSelection/useTreeViewSelection.js +82 -94
- package/modern/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.js +21 -0
- package/modern/internals/utils/tree.js +34 -4
- package/node/RichTreeView/RichTreeView.js +1 -1
- package/node/SimpleTreeView/SimpleTreeView.js +1 -1
- package/node/TreeItem/TreeItem.js +1 -1
- package/node/TreeItem/TreeItemContent.js +1 -1
- package/node/TreeItem/useTreeItemState.js +1 -3
- package/node/TreeItem2/TreeItem2.js +1 -1
- package/node/TreeItem2Icon/TreeItem2Icon.js +1 -1
- package/node/TreeView/TreeView.js +1 -1
- package/node/hooks/useTreeItem2Utils/useTreeItem2Utils.js +1 -3
- package/node/hooks/useTreeViewApiRef.js +1 -1
- package/node/icons/icons.js +1 -1
- package/node/index.js +1 -1
- package/node/internals/TreeViewProvider/TreeViewChildrenItemProvider.js +1 -1
- package/node/internals/TreeViewProvider/TreeViewContext.js +1 -1
- package/node/internals/TreeViewProvider/TreeViewProvider.js +1 -1
- package/node/internals/TreeViewProvider/useTreeViewContext.js +1 -1
- package/node/internals/corePlugins/useTreeViewInstanceEvents/useTreeViewInstanceEvents.js +1 -1
- package/node/internals/hooks/useInstanceEventHandler.js +1 -1
- package/node/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.js +1 -1
- package/node/internals/plugins/useTreeViewFocus/useTreeViewFocus.js +2 -5
- package/node/internals/plugins/useTreeViewId/useTreeViewId.js +1 -1
- package/node/internals/plugins/useTreeViewItems/useTreeViewItems.js +1 -1
- package/node/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.js +1 -1
- package/node/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.js +32 -64
- package/node/internals/plugins/useTreeViewSelection/useTreeViewSelection.js +82 -94
- package/node/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.js +29 -0
- package/node/internals/useTreeView/useTreeView.js +1 -1
- package/node/internals/useTreeView/useTreeViewModels.js +1 -1
- package/node/internals/utils/tree.js +37 -5
- package/package.json +3 -5
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform the `selectedItems` model to be an array if it was a string or null.
|
|
3
|
+
* @param {string[] | string | null} model The raw model.
|
|
4
|
+
* @returns {string[]} The converted model.
|
|
5
|
+
*/
|
|
6
|
+
export const convertSelectedItemsToArray = model => {
|
|
7
|
+
if (Array.isArray(model)) {
|
|
8
|
+
return model;
|
|
9
|
+
}
|
|
10
|
+
if (model != null) {
|
|
11
|
+
return [model];
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
};
|
|
15
|
+
export const getLookupFromArray = array => {
|
|
16
|
+
const lookup = {};
|
|
17
|
+
array.forEach(itemId => {
|
|
18
|
+
lookup[itemId] = true;
|
|
19
|
+
});
|
|
20
|
+
return lookup;
|
|
21
|
+
};
|
|
@@ -11,7 +11,7 @@ export interface UseTreeViewBaseParameters<TPlugins extends readonly TreeViewPlu
|
|
|
11
11
|
slotProps: MergePluginsProperty<ConvertPluginsIntoSignatures<TPlugins>, 'slotProps'>;
|
|
12
12
|
}
|
|
13
13
|
export type UseTreeViewDefaultizedParameters<TPlugins extends readonly TreeViewPlugin<TreeViewAnyPluginSignature>[]> = UseTreeViewBaseParameters<TPlugins> & MergePluginsProperty<ConvertPluginsIntoSignatures<TPlugins>, 'defaultizedParams'>;
|
|
14
|
-
export interface UseTreeViewRootSlotProps extends Pick<React.HTMLAttributes<HTMLUListElement>, 'onFocus' | 'onBlur' | 'onKeyDown' | 'id' | 'aria-
|
|
14
|
+
export interface UseTreeViewRootSlotProps extends Pick<React.HTMLAttributes<HTMLUListElement>, 'onFocus' | 'onBlur' | 'onKeyDown' | 'id' | 'aria-multiselectable' | 'role' | 'tabIndex'> {
|
|
15
15
|
ref: React.Ref<HTMLUListElement>;
|
|
16
16
|
}
|
|
17
17
|
export interface UseTreeViewReturnValue<TPlugins extends readonly TreeViewAnyPluginSignature[]> {
|
|
@@ -3,4 +3,4 @@ import { TreeViewAnyPluginSignature, TreeViewPlugin, ConvertPluginsIntoSignature
|
|
|
3
3
|
* Implements the same behavior as `useControlled` but for several models.
|
|
4
4
|
* The controlled models are never stored in the state and the state is only updated if the model is not controlled.
|
|
5
5
|
*/
|
|
6
|
-
export declare const useTreeViewModels: <TPlugins extends readonly TreeViewPlugin<TreeViewAnyPluginSignature>[]>(plugins: TPlugins, props: MergePluginsProperty<ConvertPluginsIntoSignatures<TPlugins>,
|
|
6
|
+
export declare const useTreeViewModels: <TPlugins extends readonly TreeViewPlugin<TreeViewAnyPluginSignature>[]>(plugins: TPlugins, props: MergePluginsProperty<ConvertPluginsIntoSignatures<TPlugins>, 'defaultizedParams'>) => MergePluginsProperty<ConvertPluginsIntoSignatures<TPlugins>, "models">;
|
|
@@ -8,7 +8,7 @@ export declare const extractPluginParamsFromProps: <TPlugins extends readonly Tr
|
|
|
8
8
|
}>({ props: { slots, slotProps, apiRef, ...props }, plugins, rootRef, }: {
|
|
9
9
|
props: TProps;
|
|
10
10
|
plugins: TPlugins;
|
|
11
|
-
rootRef?: React.Ref<HTMLUListElement
|
|
11
|
+
rootRef?: React.Ref<HTMLUListElement>;
|
|
12
12
|
}) => {
|
|
13
13
|
pluginParams: UseTreeViewBaseParameters<TPlugins> & MergePluginsProperty<ConvertPluginsIntoSignatures<TPlugins>, "params">;
|
|
14
14
|
slots: TSlots | undefined;
|
|
@@ -2,4 +2,4 @@ import { UseTreeViewInstanceEventsInstance } from '../corePlugins/useTreeViewIns
|
|
|
2
2
|
import { TreeViewAnyPluginSignature, TreeViewUsedEvents } from '../models';
|
|
3
3
|
export declare const publishTreeViewEvent: <Instance extends UseTreeViewInstanceEventsInstance & {
|
|
4
4
|
$$signature: TreeViewAnyPluginSignature;
|
|
5
|
-
}, E extends keyof Instance["$$signature"]["events"] | keyof import("../models").MergePluginsProperty<[import("../corePlugins").TreeViewCorePluginsSignature, ...Instance["$$signature"]["dependantPlugins"]], "events">>(instance: Instance, eventName: E, params: TreeViewUsedEvents<Instance[
|
|
5
|
+
}, E extends keyof Instance["$$signature"]["events"] | keyof import("../models").MergePluginsProperty<[import("../corePlugins").TreeViewCorePluginsSignature, ...Instance["$$signature"]["dependantPlugins"]], "events">>(instance: Instance, eventName: E, params: TreeViewUsedEvents<Instance['$$signature']>[E]['params']) => void;
|
|
@@ -5,4 +5,20 @@ export declare const getPreviousNavigableItem: (instance: TreeViewInstance<[UseT
|
|
|
5
5
|
export declare const getNextNavigableItem: (instance: TreeViewInstance<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, itemId: string) => string | null;
|
|
6
6
|
export declare const getLastNavigableItem: (instance: TreeViewInstance<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>) => string;
|
|
7
7
|
export declare const getFirstNavigableItem: (instance: TreeViewInstance<[UseTreeViewItemsSignature]>) => string;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* This is used to determine the start and end of a selection range so
|
|
10
|
+
* we can get the items between the two border items.
|
|
11
|
+
*
|
|
12
|
+
* It finds the items' common ancestor using
|
|
13
|
+
* a naive implementation of a lowest common ancestor algorithm
|
|
14
|
+
* (https://en.wikipedia.org/wiki/Lowest_common_ancestor).
|
|
15
|
+
* Then compares the ancestor's 2 children that are ancestors of itemA and ItemB
|
|
16
|
+
* so we can compare their indexes to work out which item comes first in a depth first search.
|
|
17
|
+
* (https://en.wikipedia.org/wiki/Depth-first_search)
|
|
18
|
+
*
|
|
19
|
+
* Another way to put it is which item is shallower in a trémaux tree
|
|
20
|
+
* https://en.wikipedia.org/wiki/Tr%C3%A9maux_tree
|
|
21
|
+
*/
|
|
22
|
+
export declare const findOrderInTremauxTree: (instance: TreeViewInstance<[UseTreeViewItemsSignature]>, itemAId: string, itemBId: string) => string[];
|
|
23
|
+
export declare const getNonDisabledItemsInRange: (instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, itemAId: string, itemBId: string) => string[];
|
|
24
|
+
export declare const getAllNavigableItems: (instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>) => string[];
|
package/internals/utils/tree.js
CHANGED
|
@@ -84,7 +84,7 @@ export const getFirstNavigableItem = instance => instance.getItemOrderedChildren
|
|
|
84
84
|
* Another way to put it is which item is shallower in a trémaux tree
|
|
85
85
|
* https://en.wikipedia.org/wiki/Tr%C3%A9maux_tree
|
|
86
86
|
*/
|
|
87
|
-
const findOrderInTremauxTree = (instance, itemAId, itemBId) => {
|
|
87
|
+
export const findOrderInTremauxTree = (instance, itemAId, itemBId) => {
|
|
88
88
|
if (itemAId === itemBId) {
|
|
89
89
|
return [itemAId, itemBId];
|
|
90
90
|
}
|
|
@@ -125,13 +125,43 @@ const findOrderInTremauxTree = (instance, itemAId, itemBId) => {
|
|
|
125
125
|
const bSide = bFamily[bFamily.indexOf(commonAncestor) - 1];
|
|
126
126
|
return ancestorFamily.indexOf(aSide) < ancestorFamily.indexOf(bSide) ? [itemAId, itemBId] : [itemBId, itemAId];
|
|
127
127
|
};
|
|
128
|
-
export const
|
|
128
|
+
export const getNonDisabledItemsInRange = (instance, itemAId, itemBId) => {
|
|
129
|
+
const getNextItem = itemId => {
|
|
130
|
+
// If the item is expanded and has some children, return the first of them.
|
|
131
|
+
if (instance.isItemExpandable(itemId) && instance.isItemExpanded(itemId)) {
|
|
132
|
+
return instance.getItemOrderedChildrenIds(itemId)[0];
|
|
133
|
+
}
|
|
134
|
+
let itemMeta = instance.getItemMeta(itemId);
|
|
135
|
+
while (itemMeta != null) {
|
|
136
|
+
// Try to find the first navigable sibling after the current item.
|
|
137
|
+
const siblings = instance.getItemOrderedChildrenIds(itemMeta.parentId);
|
|
138
|
+
const currentItemIndex = instance.getItemIndex(itemMeta.id);
|
|
139
|
+
if (currentItemIndex < siblings.length - 1) {
|
|
140
|
+
return siblings[currentItemIndex + 1];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If the item is the last of its siblings, go up a level to the parent and try again.
|
|
144
|
+
itemMeta = instance.getItemMeta(itemMeta.parentId);
|
|
145
|
+
}
|
|
146
|
+
throw new Error('Invalid range');
|
|
147
|
+
};
|
|
129
148
|
const [first, last] = findOrderInTremauxTree(instance, itemAId, itemBId);
|
|
130
149
|
const items = [first];
|
|
131
150
|
let current = first;
|
|
132
151
|
while (current !== last) {
|
|
133
|
-
current =
|
|
134
|
-
|
|
152
|
+
current = getNextItem(current);
|
|
153
|
+
if (!instance.isItemDisabled(current)) {
|
|
154
|
+
items.push(current);
|
|
155
|
+
}
|
|
135
156
|
}
|
|
136
157
|
return items;
|
|
158
|
+
};
|
|
159
|
+
export const getAllNavigableItems = instance => {
|
|
160
|
+
let item = getFirstNavigableItem(instance);
|
|
161
|
+
const navigableItems = [];
|
|
162
|
+
while (item != null) {
|
|
163
|
+
navigableItems.push(item);
|
|
164
|
+
item = getNextNavigableItem(instance, item);
|
|
165
|
+
}
|
|
166
|
+
return navigableItems;
|
|
137
167
|
};
|
|
@@ -19,8 +19,7 @@ import { treeItemClasses, getTreeItemUtilityClass } from './treeItemClasses';
|
|
|
19
19
|
import { useTreeViewContext } from '../internals/TreeViewProvider/useTreeViewContext';
|
|
20
20
|
import { TreeViewCollapseIcon, TreeViewExpandIcon } from '../icons';
|
|
21
21
|
import { TreeItem2Provider } from '../TreeItem2Provider';
|
|
22
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
23
|
-
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
24
23
|
const useUtilityClasses = ownerState => {
|
|
25
24
|
const {
|
|
26
25
|
classes
|
|
@@ -5,8 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
import PropTypes from 'prop-types';
|
|
6
6
|
import clsx from 'clsx';
|
|
7
7
|
import { useTreeItemState } from './useTreeItemState';
|
|
8
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
-
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
9
|
/**
|
|
11
10
|
* @ignore - internal component.
|
|
12
11
|
*/
|
|
@@ -32,9 +32,7 @@ export function useTreeItemState(itemId) {
|
|
|
32
32
|
const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey);
|
|
33
33
|
if (multiple) {
|
|
34
34
|
if (event.shiftKey) {
|
|
35
|
-
instance.
|
|
36
|
-
end: itemId
|
|
37
|
-
});
|
|
35
|
+
instance.expandSelectionRange(event, itemId);
|
|
38
36
|
} else {
|
|
39
37
|
instance.selectItem(event, itemId, true);
|
|
40
38
|
}
|
|
@@ -14,8 +14,7 @@ import { unstable_useTreeItem2 as useTreeItem2 } from '../useTreeItem2';
|
|
|
14
14
|
import { getTreeItemUtilityClass, treeItemClasses } from '../TreeItem';
|
|
15
15
|
import { TreeItem2Icon } from '../TreeItem2Icon';
|
|
16
16
|
import { TreeItem2Provider } from '../TreeItem2Provider';
|
|
17
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
18
|
export const TreeItem2Root = styled('li', {
|
|
20
19
|
name: 'MuiTreeItem2',
|
|
21
20
|
slot: 'Root',
|
|
@@ -40,9 +40,7 @@ export const useTreeItem2Utils = ({
|
|
|
40
40
|
const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey);
|
|
41
41
|
if (multiple) {
|
|
42
42
|
if (event.shiftKey) {
|
|
43
|
-
instance.
|
|
44
|
-
end: itemId
|
|
45
|
-
});
|
|
43
|
+
instance.expandSelectionRange(event, itemId);
|
|
46
44
|
} else {
|
|
47
45
|
instance.selectItem(event, itemId, true);
|
|
48
46
|
}
|
package/modern/index.js
CHANGED
|
@@ -105,12 +105,9 @@ export const useTreeViewFocus = ({
|
|
|
105
105
|
instance.focusDefaultItem(event);
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
|
-
const focusedItem = instance.getItemMeta(state.focusedItemId);
|
|
109
|
-
const activeDescendant = focusedItem ? instance.getTreeItemIdAttribute(focusedItem.id, focusedItem.idAttribute) : null;
|
|
110
108
|
return {
|
|
111
109
|
getRootProps: otherHandlers => ({
|
|
112
|
-
onFocus: createRootHandleFocus(otherHandlers)
|
|
113
|
-
'aria-activedescendant': activeDescendant ?? undefined
|
|
110
|
+
onFocus: createRootHandleFocus(otherHandlers)
|
|
114
111
|
}),
|
|
115
112
|
publicAPI: {
|
|
116
113
|
focusItem
|
package/modern/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.js
CHANGED
|
@@ -5,14 +5,6 @@ import { getFirstNavigableItem, getLastNavigableItem, getNextNavigableItem, getP
|
|
|
5
5
|
function isPrintableCharacter(string) {
|
|
6
6
|
return !!string && string.length === 1 && !!string.match(/\S/);
|
|
7
7
|
}
|
|
8
|
-
function findNextFirstChar(firstChars, startIndex, char) {
|
|
9
|
-
for (let i = startIndex; i < firstChars.length; i += 1) {
|
|
10
|
-
if (char === firstChars[i]) {
|
|
11
|
-
return i;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return -1;
|
|
15
|
-
}
|
|
16
8
|
export const useTreeViewKeyboardNavigation = ({
|
|
17
9
|
instance,
|
|
18
10
|
params,
|
|
@@ -35,42 +27,29 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
35
27
|
Object.values(state.items.itemMetaMap).forEach(processItem);
|
|
36
28
|
firstCharMap.current = newFirstCharMap;
|
|
37
29
|
}, [state.items.itemMetaMap, params.getItemId, instance]);
|
|
38
|
-
const getFirstMatchingItem = (itemId,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
30
|
+
const getFirstMatchingItem = (itemId, query) => {
|
|
31
|
+
const cleanQuery = query.toLowerCase();
|
|
32
|
+
const getNextItem = itemIdToCheck => {
|
|
33
|
+
const nextItemId = getNextNavigableItem(instance, itemIdToCheck);
|
|
34
|
+
// We reached the end of the tree, check from the beginning
|
|
35
|
+
if (nextItemId === null) {
|
|
36
|
+
return getFirstNavigableItem(instance);
|
|
37
|
+
}
|
|
38
|
+
return nextItemId;
|
|
39
|
+
};
|
|
40
|
+
let matchingItemId = null;
|
|
41
|
+
let currentItemId = getNextItem(itemId);
|
|
42
|
+
const checkedItems = {};
|
|
43
|
+
// The "!checkedItems[currentItemId]" condition avoids an infinite loop when there is no matching item.
|
|
44
|
+
while (matchingItemId == null && !checkedItems[currentItemId]) {
|
|
45
|
+
if (firstCharMap.current[currentItemId] === cleanQuery) {
|
|
46
|
+
matchingItemId = currentItemId;
|
|
47
|
+
} else {
|
|
48
|
+
checkedItems[currentItemId] = true;
|
|
49
|
+
currentItemId = getNextItem(currentItemId);
|
|
52
50
|
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Get start index for search based on position of currentItem
|
|
56
|
-
start = firstCharIds.indexOf(itemId) + 1;
|
|
57
|
-
if (start >= firstCharIds.length) {
|
|
58
|
-
start = 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check remaining slots in the menu
|
|
62
|
-
index = findNextFirstChar(firstChars, start, lowercaseChar);
|
|
63
|
-
|
|
64
|
-
// If not found in remaining slots, check from beginning
|
|
65
|
-
if (index === -1) {
|
|
66
|
-
index = findNextFirstChar(firstChars, 0, lowercaseChar);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// If a match was found...
|
|
70
|
-
if (index > -1) {
|
|
71
|
-
return firstCharIds[index];
|
|
72
51
|
}
|
|
73
|
-
return
|
|
52
|
+
return matchingItemId;
|
|
74
53
|
};
|
|
75
54
|
const canToggleItemSelection = itemId => !params.disableSelection && !instance.isItemDisabled(itemId);
|
|
76
55
|
const canToggleItemExpansion = itemId => {
|
|
@@ -95,9 +74,7 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
95
74
|
{
|
|
96
75
|
event.preventDefault();
|
|
97
76
|
if (params.multiSelect && event.shiftKey) {
|
|
98
|
-
instance.
|
|
99
|
-
end: itemId
|
|
100
|
-
});
|
|
77
|
+
instance.expandSelectionRange(event, itemId);
|
|
101
78
|
} else if (params.multiSelect) {
|
|
102
79
|
instance.selectItem(event, itemId, true);
|
|
103
80
|
} else {
|
|
@@ -136,10 +113,7 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
136
113
|
// Multi select behavior when pressing Shift + ArrowDown
|
|
137
114
|
// Toggles the selection state of the next item
|
|
138
115
|
if (params.multiSelect && event.shiftKey && canToggleItemSelection(nextItem)) {
|
|
139
|
-
instance.
|
|
140
|
-
end: nextItem,
|
|
141
|
-
current: itemId
|
|
142
|
-
}, true);
|
|
116
|
+
instance.selectItemFromArrowNavigation(event, itemId, nextItem);
|
|
143
117
|
}
|
|
144
118
|
}
|
|
145
119
|
break;
|
|
@@ -156,10 +130,7 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
156
130
|
// Multi select behavior when pressing Shift + ArrowUp
|
|
157
131
|
// Toggles the selection state of the previous item
|
|
158
132
|
if (params.multiSelect && event.shiftKey && canToggleItemSelection(previousItem)) {
|
|
159
|
-
instance.
|
|
160
|
-
end: previousItem,
|
|
161
|
-
current: itemId
|
|
162
|
-
}, true);
|
|
133
|
+
instance.selectItemFromArrowNavigation(event, itemId, previousItem);
|
|
163
134
|
}
|
|
164
135
|
}
|
|
165
136
|
break;
|
|
@@ -202,12 +173,12 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
202
173
|
// Focuses the first item in the tree
|
|
203
174
|
case key === 'Home':
|
|
204
175
|
{
|
|
205
|
-
instance.focusItem(event, getFirstNavigableItem(instance));
|
|
206
|
-
|
|
207
176
|
// Multi select behavior when pressing Ctrl + Shift + Home
|
|
208
177
|
// Selects the focused item and all items up to the first item.
|
|
209
178
|
if (canToggleItemSelection(itemId) && params.multiSelect && ctrlPressed && event.shiftKey) {
|
|
210
|
-
instance.
|
|
179
|
+
instance.selectRangeFromStartToItem(event, itemId);
|
|
180
|
+
} else {
|
|
181
|
+
instance.focusItem(event, getFirstNavigableItem(instance));
|
|
211
182
|
}
|
|
212
183
|
event.preventDefault();
|
|
213
184
|
break;
|
|
@@ -216,12 +187,12 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
216
187
|
// Focuses the last item in the tree
|
|
217
188
|
case key === 'End':
|
|
218
189
|
{
|
|
219
|
-
instance.focusItem(event, getLastNavigableItem(instance));
|
|
220
|
-
|
|
221
190
|
// Multi select behavior when pressing Ctrl + Shirt + End
|
|
222
191
|
// Selects the focused item and all the items down to the last item.
|
|
223
192
|
if (canToggleItemSelection(itemId) && params.multiSelect && ctrlPressed && event.shiftKey) {
|
|
224
|
-
instance.
|
|
193
|
+
instance.selectRangeFromItemToEnd(event, itemId);
|
|
194
|
+
} else {
|
|
195
|
+
instance.focusItem(event, getLastNavigableItem(instance));
|
|
225
196
|
}
|
|
226
197
|
event.preventDefault();
|
|
227
198
|
break;
|
|
@@ -239,10 +210,7 @@ export const useTreeViewKeyboardNavigation = ({
|
|
|
239
210
|
// Selects all the items
|
|
240
211
|
case key === 'a' && ctrlPressed && params.multiSelect && !params.disableSelection:
|
|
241
212
|
{
|
|
242
|
-
instance.
|
|
243
|
-
start: getFirstNavigableItem(instance),
|
|
244
|
-
end: getLastNavigableItem(instance)
|
|
245
|
-
});
|
|
213
|
+
instance.selectAllNavigableItems(event);
|
|
246
214
|
event.preventDefault();
|
|
247
215
|
break;
|
|
248
216
|
}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { getFirstNavigableItem, getLastNavigableItem,
|
|
3
|
+
import { findOrderInTremauxTree, getAllNavigableItems, getFirstNavigableItem, getLastNavigableItem, getNonDisabledItemsInRange } from '../../utils/tree';
|
|
4
|
+
import { convertSelectedItemsToArray, getLookupFromArray } from './useTreeViewSelection.utils';
|
|
4
5
|
export const useTreeViewSelection = ({
|
|
5
6
|
instance,
|
|
6
7
|
params,
|
|
7
8
|
models
|
|
8
9
|
}) => {
|
|
9
10
|
const lastSelectedItem = React.useRef(null);
|
|
10
|
-
const
|
|
11
|
-
const
|
|
11
|
+
const lastSelectedRange = React.useRef({});
|
|
12
|
+
const selectedItemsMap = React.useMemo(() => {
|
|
13
|
+
const temp = new Map();
|
|
14
|
+
if (Array.isArray(models.selectedItems.value)) {
|
|
15
|
+
models.selectedItems.value.forEach(id => {
|
|
16
|
+
temp.set(id, true);
|
|
17
|
+
});
|
|
18
|
+
} else if (models.selectedItems.value != null) {
|
|
19
|
+
temp.set(models.selectedItems.value, true);
|
|
20
|
+
}
|
|
21
|
+
return temp;
|
|
22
|
+
}, [models.selectedItems.value]);
|
|
12
23
|
const setSelectedItems = (event, newSelectedItems) => {
|
|
13
24
|
if (params.onItemSelectionToggle) {
|
|
14
25
|
if (params.multiSelect) {
|
|
@@ -34,115 +45,90 @@ export const useTreeViewSelection = ({
|
|
|
34
45
|
}
|
|
35
46
|
models.selectedItems.setControlledValue(newSelectedItems);
|
|
36
47
|
};
|
|
37
|
-
const isItemSelected = itemId =>
|
|
48
|
+
const isItemSelected = itemId => selectedItemsMap.has(itemId);
|
|
38
49
|
const selectItem = (event, itemId, multiple = false) => {
|
|
39
50
|
if (params.disableSelection) {
|
|
40
51
|
return;
|
|
41
52
|
}
|
|
53
|
+
let newSelected;
|
|
42
54
|
if (multiple) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
newSelected = [itemId].concat(models.selectedItems.value);
|
|
49
|
-
}
|
|
50
|
-
setSelectedItems(event, newSelected);
|
|
55
|
+
const cleanSelectedItems = convertSelectedItemsToArray(models.selectedItems.value);
|
|
56
|
+
if (instance.isItemSelected(itemId)) {
|
|
57
|
+
newSelected = cleanSelectedItems.filter(id => id !== itemId);
|
|
58
|
+
} else {
|
|
59
|
+
newSelected = [itemId].concat(cleanSelectedItems);
|
|
51
60
|
}
|
|
52
61
|
} else {
|
|
53
|
-
|
|
54
|
-
setSelectedItems(event, newSelected);
|
|
62
|
+
newSelected = params.multiSelect ? [itemId] : itemId;
|
|
55
63
|
}
|
|
64
|
+
setSelectedItems(event, newSelected);
|
|
56
65
|
lastSelectedItem.current = itemId;
|
|
57
|
-
|
|
58
|
-
currentRangeSelection.current = [];
|
|
66
|
+
lastSelectedRange.current = {};
|
|
59
67
|
};
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const {
|
|
63
|
-
start,
|
|
64
|
-
next,
|
|
65
|
-
current
|
|
66
|
-
} = items;
|
|
67
|
-
if (!next || !current) {
|
|
68
|
+
const selectRange = (event, [start, end]) => {
|
|
69
|
+
if (params.disableSelection || !params.multiSelect) {
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
let newSelectedItems = convertSelectedItemsToArray(models.selectedItems.value).slice();
|
|
73
|
+
|
|
74
|
+
// If the last selection was a range selection,
|
|
75
|
+
// remove the items that were part of the last range from the model
|
|
76
|
+
if (Object.keys(lastSelectedRange.current).length > 0) {
|
|
77
|
+
newSelectedItems = newSelectedItems.filter(id => !lastSelectedRange.current[id]);
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} else {
|
|
82
|
-
base.push(next);
|
|
83
|
-
currentRangeSelection.current.push(current, next);
|
|
84
|
-
}
|
|
85
|
-
setSelectedItems(event, base);
|
|
79
|
+
|
|
80
|
+
// Add to the model the items that are part of the new range and not already part of the model.
|
|
81
|
+
const selectedItemsLookup = getLookupFromArray(newSelectedItems);
|
|
82
|
+
const range = getNonDisabledItemsInRange(instance, start, end);
|
|
83
|
+
const itemsToAddToModel = range.filter(id => !selectedItemsLookup[id]);
|
|
84
|
+
newSelectedItems = newSelectedItems.concat(itemsToAddToModel);
|
|
85
|
+
setSelectedItems(event, newSelectedItems);
|
|
86
|
+
lastSelectedRange.current = getLookupFromArray(range);
|
|
86
87
|
};
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
start,
|
|
91
|
-
end
|
|
92
|
-
} = items;
|
|
93
|
-
// If last selection was a range selection ignore items that were selected.
|
|
94
|
-
if (lastSelectionWasRange.current) {
|
|
95
|
-
base = base.filter(id => currentRangeSelection.current.indexOf(id) === -1);
|
|
88
|
+
const expandSelectionRange = (event, itemId) => {
|
|
89
|
+
if (lastSelectedItem.current != null) {
|
|
90
|
+
const [start, end] = findOrderInTremauxTree(instance, itemId, lastSelectedItem.current);
|
|
91
|
+
selectRange(event, [start, end]);
|
|
96
92
|
}
|
|
97
|
-
let range = getNavigableItemsInRange(instance, start, end);
|
|
98
|
-
range = range.filter(item => !instance.isItemDisabled(item));
|
|
99
|
-
currentRangeSelection.current = range;
|
|
100
|
-
let newSelected = base.concat(range);
|
|
101
|
-
newSelected = newSelected.filter((id, i) => newSelected.indexOf(id) === i);
|
|
102
|
-
setSelectedItems(event, newSelected);
|
|
103
93
|
};
|
|
104
|
-
const
|
|
105
|
-
|
|
94
|
+
const selectRangeFromStartToItem = (event, itemId) => {
|
|
95
|
+
selectRange(event, [getFirstNavigableItem(instance), itemId]);
|
|
96
|
+
};
|
|
97
|
+
const selectRangeFromItemToEnd = (event, itemId) => {
|
|
98
|
+
selectRange(event, [itemId, getLastNavigableItem(instance)]);
|
|
99
|
+
};
|
|
100
|
+
const selectAllNavigableItems = event => {
|
|
101
|
+
if (params.disableSelection || !params.multiSelect) {
|
|
106
102
|
return;
|
|
107
103
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
current
|
|
112
|
-
} = items;
|
|
113
|
-
if (stacked) {
|
|
114
|
-
handleRangeArrowSelect(event, {
|
|
115
|
-
start,
|
|
116
|
-
next: end,
|
|
117
|
-
current
|
|
118
|
-
});
|
|
119
|
-
} else if (start != null && end != null) {
|
|
120
|
-
handleRangeSelect(event, {
|
|
121
|
-
start,
|
|
122
|
-
end
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
lastSelectionWasRange.current = true;
|
|
104
|
+
const navigableItems = getAllNavigableItems(instance);
|
|
105
|
+
setSelectedItems(event, navigableItems);
|
|
106
|
+
lastSelectedRange.current = getLookupFromArray(navigableItems);
|
|
126
107
|
};
|
|
127
|
-
const
|
|
128
|
-
if (!
|
|
129
|
-
|
|
108
|
+
const selectItemFromArrowNavigation = (event, currentItem, nextItem) => {
|
|
109
|
+
if (params.disableSelection || !params.multiSelect) {
|
|
110
|
+
return;
|
|
130
111
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
112
|
+
let newSelectedItems = convertSelectedItemsToArray(models.selectedItems.value).slice();
|
|
113
|
+
if (Object.keys(lastSelectedRange.current).length === 0) {
|
|
114
|
+
newSelectedItems.push(nextItem);
|
|
115
|
+
lastSelectedRange.current = {
|
|
116
|
+
[currentItem]: true,
|
|
117
|
+
[nextItem]: true
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
if (!lastSelectedRange.current[currentItem]) {
|
|
121
|
+
lastSelectedRange.current = {};
|
|
122
|
+
}
|
|
123
|
+
if (lastSelectedRange.current[nextItem]) {
|
|
124
|
+
newSelectedItems = newSelectedItems.filter(id => id !== currentItem);
|
|
125
|
+
delete lastSelectedRange.current[currentItem];
|
|
126
|
+
} else {
|
|
127
|
+
newSelectedItems.push(nextItem);
|
|
128
|
+
lastSelectedRange.current[nextItem] = true;
|
|
129
|
+
}
|
|
140
130
|
}
|
|
141
|
-
|
|
142
|
-
instance.selectRange(event, {
|
|
143
|
-
start,
|
|
144
|
-
end: getLastNavigableItem(instance)
|
|
145
|
-
});
|
|
131
|
+
setSelectedItems(event, newSelectedItems);
|
|
146
132
|
};
|
|
147
133
|
return {
|
|
148
134
|
getRootProps: () => ({
|
|
@@ -151,9 +137,11 @@ export const useTreeViewSelection = ({
|
|
|
151
137
|
instance: {
|
|
152
138
|
isItemSelected,
|
|
153
139
|
selectItem,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
140
|
+
selectAllNavigableItems,
|
|
141
|
+
expandSelectionRange,
|
|
142
|
+
selectRangeFromStartToItem,
|
|
143
|
+
selectRangeFromItemToEnd,
|
|
144
|
+
selectItemFromArrowNavigation
|
|
157
145
|
},
|
|
158
146
|
contextValue: {
|
|
159
147
|
selection: {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform the `selectedItems` model to be an array if it was a string or null.
|
|
3
|
+
* @param {string[] | string | null} model The raw model.
|
|
4
|
+
* @returns {string[]} The converted model.
|
|
5
|
+
*/
|
|
6
|
+
export const convertSelectedItemsToArray = model => {
|
|
7
|
+
if (Array.isArray(model)) {
|
|
8
|
+
return model;
|
|
9
|
+
}
|
|
10
|
+
if (model != null) {
|
|
11
|
+
return [model];
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
};
|
|
15
|
+
export const getLookupFromArray = array => {
|
|
16
|
+
const lookup = {};
|
|
17
|
+
array.forEach(itemId => {
|
|
18
|
+
lookup[itemId] = true;
|
|
19
|
+
});
|
|
20
|
+
return lookup;
|
|
21
|
+
};
|