Allow to hide any item in the tree (#1293)

Ref https://github.com/webstudio-is/webstudio-builder/issues/1116

We need this to hide Fragment instance in navigator which is an implicit
child of slot with persistent id.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-03-26 18:44:29 +07:00
1 parent 407955c6c0
commit f53c839fe6
8 files changed
+76 -51

No files matched your search

@@ -207,8 +207,7 @@ const PagesPanel = ({
</>
}
/>
<TreeNode
hideRoot
<TreeNode<PagesTreeNode>
selectedItemSelector={[selectedPageId, pagesTree.id]}
onSelect={selectTreeNode}
itemData={pagesTree}
@@ -219,6 +218,7 @@ const PagesPanel = ({
}
return [];
}}
isItemHidden={(itemId) => itemId === pagesTree.id}
getIsExpanded={() => true}
/>
</Box>
@@ -1,5 +1,6 @@
import { useCallback } from "react";
import { useStore } from "@nanostores/react";
import type { Instance } from "@webstudio-is/project-build";
import {
hoveredInstanceSelectorStore,
rootInstanceStore,
@@ -7,9 +8,9 @@ import {
useDragAndDropState,
} from "~/shared/nano-states";
import type { InstanceSelector } from "~/shared/tree-utils";
import { InstanceTree } from "./tree";
import { reparentInstance } from "~/shared/instance-utils";
import { textEditingInstanceSelectorStore } from "~/shared/nano-states/instances";
import { InstanceTree } from "./tree";
export const NavigatorTree = () => {
const selectedInstanceSelector = useStore(selectedInstanceSelectorStore);
@@ -21,6 +22,8 @@ export const NavigatorTree = () => {
? state.dragPayload.dragInstanceSelector
: undefined;
const isItemHidden = useCallback((_instanceId: Instance["id"]) => false, []);
const handleDragEnd = useCallback(
(payload: {
itemSelector: InstanceSelector;
@@ -50,6 +53,7 @@ export const NavigatorTree = () => {
selectedItemSelector={selectedInstanceSelector}
dragItemSelector={dragItemSelector}
dropTarget={state.dropTarget}
isItemHidden={isItemHidden}
onSelect={handleSelect}
onHover={hoveredInstanceSelectorStore.set}
onDragItemChange={(dragInstanceSelector) => {
@@ -116,6 +116,7 @@ const render = (
findItemById(tree, itemId)?.canAcceptChildren ?? false,
getItemChildren: (itemId: ItemId) =>
findItemById(tree, itemId)?.children ?? [],
isItemHidden: (_itemId: ItemId) => false,
},
});
@@ -15,10 +15,12 @@ export const useHorizontalShift = <Data extends { id: ItemId }>({
placementIndicator,
getIsExpanded,
canAcceptChild,
isItemHidden,
getItemChildren,
}: {
getItemChildren: (itemId: ItemId) => Data[];
canAcceptChild: (itemId: ItemId) => boolean;
isItemHidden: (itemId: ItemId) => boolean;
dragItemSelector: undefined | ItemSelector;
dropTarget: ItemDropTarget | undefined;
placementIndicator: undefined | Placement;
@@ -37,8 +39,6 @@ export const useHorizontalShift = <Data extends { id: ItemId }>({
return undefined;
}
const dragItemDepth = dragItemSelector.length - 1;
const { itemSelector: dropItemSelector, indexWithinChildren } = dropTarget;
const shiftPlacement = (depth: number) => {
@@ -57,13 +57,21 @@ export const useHorizontalShift = <Data extends { id: ItemId }>({
return { itemSelector: dropItemSelector, position: "end" };
}
let dropHiddenCount = 0;
for (const itemId of dropItemSelector) {
if (isItemHidden(itemId)) {
dropHiddenCount += 1;
}
}
const dragItemDepth = dragItemSelector.length - 1;
const currentDepth = dropItemSelector.length;
const desiredDepth = dragItemDepth + horizontalShift;
const withoutShift = {
itemSelector: dropItemSelector,
position: indexWithinChildren,
placement: shiftPlacement(currentDepth),
placement: shiftPlacement(currentDepth - dropHiddenCount),
} as const;
const [dragItemId] = dragItemSelector;
@@ -110,7 +118,7 @@ export const useHorizontalShift = <Data extends { id: ItemId }>({
return {
itemSelector: newParentSelector,
position: newPosition,
placement: shiftPlacement(currentDepth - shifted),
placement: shiftPlacement(currentDepth - dropHiddenCount - shifted),
};
}
@@ -155,7 +163,7 @@ export const useHorizontalShift = <Data extends { id: ItemId }>({
return {
itemSelector: newParentSelector,
position: "end",
placement: shiftPlacement(currentDepth + shifted),
placement: shiftPlacement(currentDepth - dropHiddenCount + shifted),
};
}
@@ -166,6 +174,7 @@ export const useHorizontalShift = <Data extends { id: ItemId }>({
dragItemSelector,
horizontalShift,
canAcceptChild,
isItemHidden,
getItemChildren,
getIsExpanded,
]);
@@ -4,6 +4,7 @@ import type { ItemSelector } from "./item-utils";
export type Item = {
id: string;
canAcceptChildren: boolean;
isHidden?: boolean;
children: Item[];
};
@@ -395,6 +395,7 @@ export const TreeItemLabel = ({
export type TreeNodeProps<Data extends { id: ItemId }> = {
itemData: Data;
getItemChildren: (itemId: ItemId) => Data[];
isItemHidden: (itemId: ItemId) => boolean;
renderItem: (props: TreeItemRenderProps<Data>) => React.ReactNode;
getIsExpanded: (itemSelector: ItemSelector) => boolean;
@@ -411,14 +412,15 @@ export type TreeNodeProps<Data extends { id: ItemId }> = {
onMouseLeave?: (itemSelector: ItemSelector) => void;
animate?: boolean;
hideRoot?: boolean;
level?: number;
};
export const TreeNode = <Data extends { id: string }>({
itemData,
parentSelector,
parentIsSelected,
hideRoot = false,
level = 0,
...commonProps
}: TreeNodeProps<Data>) => {
const {
@@ -433,6 +435,7 @@ export const TreeNode = <Data extends { id: string }>({
dropTargetItemSelector,
renderItem,
getItemChildren,
isItemHidden,
} = commonProps;
const collapsibleContentRef = useRef<HTMLDivElement>(null);
@@ -441,16 +444,12 @@ export const TreeNode = <Data extends { id: string }>({
const itemSelector = [itemData.id, ...(parentSelector ?? [])];
const isAlwaysExpanded =
// root is always expanded
parentSelector === undefined ||
// or root children when root is hidden
(hideRoot && parentSelector.length === 1);
let level = parentSelector?.length ?? 0;
// reduce level when root is hidden
if (hideRoot && level !== 0) {
level -= 1;
const itemIsHidden = isItemHidden(itemData.id);
const isAlwaysExpanded = itemIsHidden;
if (itemIsHidden === false) {
level += 1;
}
const isExpandable = itemChildren.length > 0;
const isExpanded = getIsExpanded(itemSelector) || isAlwaysExpanded;
const isSelected = areItemSelectorsEqual(itemSelector, selectedItemSelector);
@@ -486,7 +485,7 @@ export const TreeNode = <Data extends { id: string }>({
data-drop-target-id={itemData.id}
>
{/* optionally prevent rendering root item */}
{(parentSelector !== undefined || hideRoot !== true) &&
{itemIsHidden === false &&
renderItem({
dropTargetItemSelector,
onMouseEnter,
@@ -515,7 +514,7 @@ export const TreeNode = <Data extends { id: string }>({
itemData={child}
parentSelector={itemSelector}
parentIsSelected={isSelected || parentIsSelected}
hideRoot={hideRoot}
level={level}
{...commonProps}
/>
))
@@ -32,22 +32,29 @@ export const StressTest = ({ animate }: { animate: boolean }) => {
canAcceptChildren: true,
children: [
{
id: `box-${index}.0.0`,
id: `hidden-${index}.0.0`,
canAcceptChildren: true,
isHidden: true,
children: [
{
id: `box-${index}.0.0.0`,
canAcceptChildren: true,
children: [
{
id: `heading-${index}.1`,
canAcceptChildren: false,
children: [],
},
{
id: `paragraph-${index}.1`,
canAcceptChildren: false,
children: [],
id: `box-${index}.0.0.0.0`,
canAcceptChildren: true,
children: [
{
id: `heading-${index}.1`,
canAcceptChildren: false,
children: [],
},
{
id: `paragraph-${index}.1`,
canAcceptChildren: false,
children: [],
},
],
},
],
},
@@ -77,11 +84,18 @@ export const StressTest = ({ animate }: { animate: boolean }) => {
return (
<Flex css={{ width: 300, height: 500, flexDirection: "column" }}>
<Tree
canAcceptChild={(itemId) =>
findItemById(root, itemId)?.canAcceptChildren ?? false
}
canAcceptChild={(itemId) => {
const item = findItemById(root, itemId);
return (
(item?.canAcceptChildren ?? false) &&
(item?.isHidden ?? false) === false
);
}}
canLeaveParent={() => true}
getItemChildren={(itemId) => findItemById(root, itemId)?.children ?? []}
isItemHidden={(itemSelector) =>
findItemById(root, itemSelector[0])?.isHidden ?? false
}
animate={animate}
root={root}
selectedItemSelector={selectedItemSelector}
@@ -31,6 +31,7 @@ export type TreeProps<Data extends { id: string }> = {
canLeaveParent: (itemId: ItemId) => boolean;
canAcceptChild: (itemId: ItemId) => boolean;
getItemChildren: (itemId: ItemId) => Data[];
isItemHidden: (itemId: ItemId) => boolean;
renderItem: (props: TreeItemRenderProps<Data>) => React.ReactNode;
onSelect?: (itemSelector: ItemSelector) => void;
@@ -66,6 +67,7 @@ export const Tree = <Data extends { id: string }>({
canLeaveParent,
canAcceptChild,
getItemChildren,
isItemHidden,
renderItem,
onSelect,
onHover,
@@ -77,7 +79,6 @@ export const Tree = <Data extends { id: string }>({
}: TreeProps<Data>) => {
const { getIsExpanded, setIsExpanded } = useExpandState({
selectedItemSelector,
getItemChildren,
});
const rootRef = useRef<HTMLElement | null>(null);
@@ -112,6 +113,7 @@ export const Tree = <Data extends { id: string }>({
placementIndicator,
getIsExpanded,
getItemChildren,
isItemHidden,
canAcceptChild,
});
@@ -131,11 +133,7 @@ export const Tree = <Data extends { id: string }>({
if (dropTarget === undefined) {
return;
}
const [itemId] = dropTarget.itemSelector;
if (
getItemChildren(itemId).length > 0 ||
getIsExpanded(dropTarget.itemSelector) === false
) {
if (getIsExpanded(dropTarget.itemSelector) === false) {
setIsExpanded(dropTarget.itemSelector, true);
}
},
@@ -261,6 +259,7 @@ export const Tree = <Data extends { id: string }>({
const keyboardNavigation = useKeyboardNavigation({
root,
getItemChildren,
isItemHidden,
selectedItemSelector,
getIsExpanded,
setIsExpanded,
@@ -293,6 +292,7 @@ export const Tree = <Data extends { id: string }>({
<TreeNode
renderItem={renderItem}
getItemChildren={getItemChildren}
isItemHidden={isItemHidden}
animate={animate}
onSelect={onSelect}
onMouseEnter={onHover}
@@ -324,6 +324,7 @@ const useKeyboardNavigation = <Data extends { id: string }>({
root,
selectedItemSelector,
getItemChildren,
isItemHidden,
getIsExpanded,
setIsExpanded,
onEsc,
@@ -331,6 +332,7 @@ const useKeyboardNavigation = <Data extends { id: string }>({
root: Data;
selectedItemSelector: undefined | ItemSelector;
getItemChildren: (itemId: ItemId) => Data[];
isItemHidden: (itemId: ItemId) => boolean;
getIsExpanded: (itemSelector: ItemSelector) => boolean;
setIsExpanded: (itemSelector: ItemSelector, isExpanded: boolean) => void;
onEsc: () => void;
@@ -339,7 +341,9 @@ const useKeyboardNavigation = <Data extends { id: string }>({
const result: ItemSelector[] = [];
const traverse = (itemSelector: ItemSelector) => {
const [itemId] = itemSelector;
result.push(itemSelector);
if (isItemHidden(itemId) === false) {
result.push(itemSelector);
}
if (getIsExpanded(itemSelector)) {
for (const child of getItemChildren(itemId)) {
traverse([child.id, ...itemSelector]);
@@ -348,7 +352,7 @@ const useKeyboardNavigation = <Data extends { id: string }>({
};
traverse([root.id]);
return result;
}, [root, getIsExpanded, getItemChildren]);
}, [root, getIsExpanded, getItemChildren, isItemHidden]);
const rootRef = useRef<HTMLDivElement>(null);
@@ -465,12 +469,10 @@ const useKeyboardNavigation = <Data extends { id: string }>({
};
};
const useExpandState = <Data extends { id: string }>({
const useExpandState = ({
selectedItemSelector,
getItemChildren,
}: {
selectedItemSelector: undefined | ItemSelector;
getItemChildren: (itemId: ItemId) => Data[];
}) => {
const [record, setRecord] = useState<Record<string, boolean>>({});
@@ -512,14 +514,9 @@ const useExpandState = <Data extends { id: string }>({
if (itemSelector.length === 1) {
return true;
}
const [itemId] = itemSelector;
return (
getItemChildren(itemId).length > 0 &&
record[itemSelector.join()] === true
);
return record[itemSelector.join()] === true;
},
[record, getItemChildren]
[record]
);
const setIsExpanded = useCallback(