From f53c839fe68dac80f23eedfc2c1ee2f6f486c19c Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 26 Mar 2023 18:44:29 +0700 Subject: [PATCH] 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. --- .../sidebar-left/panels/pages/pages.tsx | 4 +- .../app/builder/shared/navigator-tree.tsx | 6 ++- .../components/tree/horizontal-shift.test.ts | 1 + .../src/components/tree/horizontal-shift.ts | 19 +++++++--- .../src/components/tree/test-tree-data.ts | 1 + .../src/components/tree/tree-node.tsx | 25 ++++++------ .../src/components/tree/tree.stories.tsx | 38 +++++++++++++------ .../src/components/tree/tree.tsx | 33 ++++++++-------- 8 files changed, 76 insertions(+), 51 deletions(-) diff --git a/apps/builder/app/builder/features/sidebar-left/panels/pages/pages.tsx b/apps/builder/app/builder/features/sidebar-left/panels/pages/pages.tsx index 4ad800e03b..7bb6559a3a 100644 --- a/apps/builder/app/builder/features/sidebar-left/panels/pages/pages.tsx +++ b/apps/builder/app/builder/features/sidebar-left/panels/pages/pages.tsx @@ -207,8 +207,7 @@ const PagesPanel = ({ } /> - selectedItemSelector={[selectedPageId, pagesTree.id]} onSelect={selectTreeNode} itemData={pagesTree} @@ -219,6 +218,7 @@ const PagesPanel = ({ } return []; }} + isItemHidden={(itemId) => itemId === pagesTree.id} getIsExpanded={() => true} /> diff --git a/apps/builder/app/builder/shared/navigator-tree.tsx b/apps/builder/app/builder/shared/navigator-tree.tsx index f844f49e8a..cc4a860c4e 100644 --- a/apps/builder/app/builder/shared/navigator-tree.tsx +++ b/apps/builder/app/builder/shared/navigator-tree.tsx @@ -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) => { diff --git a/packages/design-system/src/components/tree/horizontal-shift.test.ts b/packages/design-system/src/components/tree/horizontal-shift.test.ts index 4ec0d02def..507a42ac00 100644 --- a/packages/design-system/src/components/tree/horizontal-shift.test.ts +++ b/packages/design-system/src/components/tree/horizontal-shift.test.ts @@ -116,6 +116,7 @@ const render = ( findItemById(tree, itemId)?.canAcceptChildren ?? false, getItemChildren: (itemId: ItemId) => findItemById(tree, itemId)?.children ?? [], + isItemHidden: (_itemId: ItemId) => false, }, }); diff --git a/packages/design-system/src/components/tree/horizontal-shift.ts b/packages/design-system/src/components/tree/horizontal-shift.ts index 38055ae037..fb42ff6c2e 100644 --- a/packages/design-system/src/components/tree/horizontal-shift.ts +++ b/packages/design-system/src/components/tree/horizontal-shift.ts @@ -15,10 +15,12 @@ export const useHorizontalShift = ({ 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 = ({ return undefined; } - const dragItemDepth = dragItemSelector.length - 1; - const { itemSelector: dropItemSelector, indexWithinChildren } = dropTarget; const shiftPlacement = (depth: number) => { @@ -57,13 +57,21 @@ export const useHorizontalShift = ({ 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 = ({ return { itemSelector: newParentSelector, position: newPosition, - placement: shiftPlacement(currentDepth - shifted), + placement: shiftPlacement(currentDepth - dropHiddenCount - shifted), }; } @@ -155,7 +163,7 @@ export const useHorizontalShift = ({ return { itemSelector: newParentSelector, position: "end", - placement: shiftPlacement(currentDepth + shifted), + placement: shiftPlacement(currentDepth - dropHiddenCount + shifted), }; } @@ -166,6 +174,7 @@ export const useHorizontalShift = ({ dragItemSelector, horizontalShift, canAcceptChild, + isItemHidden, getItemChildren, getIsExpanded, ]); diff --git a/packages/design-system/src/components/tree/test-tree-data.ts b/packages/design-system/src/components/tree/test-tree-data.ts index 4a16365237..48da84dacf 100644 --- a/packages/design-system/src/components/tree/test-tree-data.ts +++ b/packages/design-system/src/components/tree/test-tree-data.ts @@ -4,6 +4,7 @@ import type { ItemSelector } from "./item-utils"; export type Item = { id: string; canAcceptChildren: boolean; + isHidden?: boolean; children: Item[]; }; diff --git a/packages/design-system/src/components/tree/tree-node.tsx b/packages/design-system/src/components/tree/tree-node.tsx index 5451c2d721..65d6247f9b 100644 --- a/packages/design-system/src/components/tree/tree-node.tsx +++ b/packages/design-system/src/components/tree/tree-node.tsx @@ -395,6 +395,7 @@ export const TreeItemLabel = ({ export type TreeNodeProps = { itemData: Data; getItemChildren: (itemId: ItemId) => Data[]; + isItemHidden: (itemId: ItemId) => boolean; renderItem: (props: TreeItemRenderProps) => React.ReactNode; getIsExpanded: (itemSelector: ItemSelector) => boolean; @@ -411,14 +412,15 @@ export type TreeNodeProps = { onMouseLeave?: (itemSelector: ItemSelector) => void; animate?: boolean; - hideRoot?: boolean; + + level?: number; }; export const TreeNode = ({ itemData, parentSelector, parentIsSelected, - hideRoot = false, + level = 0, ...commonProps }: TreeNodeProps) => { const { @@ -433,6 +435,7 @@ export const TreeNode = ({ dropTargetItemSelector, renderItem, getItemChildren, + isItemHidden, } = commonProps; const collapsibleContentRef = useRef(null); @@ -441,16 +444,12 @@ export const TreeNode = ({ 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-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 = ({ itemData={child} parentSelector={itemSelector} parentIsSelected={isSelected || parentIsSelected} - hideRoot={hideRoot} + level={level} {...commonProps} /> )) diff --git a/packages/design-system/src/components/tree/tree.stories.tsx b/packages/design-system/src/components/tree/tree.stories.tsx index 5d4b54265c..24899b9020 100644 --- a/packages/design-system/src/components/tree/tree.stories.tsx +++ b/packages/design-system/src/components/tree/tree.stories.tsx @@ -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 ( - 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} diff --git a/packages/design-system/src/components/tree/tree.tsx b/packages/design-system/src/components/tree/tree.tsx index b59c8b14c6..60ad2f62c8 100644 --- a/packages/design-system/src/components/tree/tree.tsx +++ b/packages/design-system/src/components/tree/tree.tsx @@ -31,6 +31,7 @@ export type TreeProps = { canLeaveParent: (itemId: ItemId) => boolean; canAcceptChild: (itemId: ItemId) => boolean; getItemChildren: (itemId: ItemId) => Data[]; + isItemHidden: (itemId: ItemId) => boolean; renderItem: (props: TreeItemRenderProps) => React.ReactNode; onSelect?: (itemSelector: ItemSelector) => void; @@ -66,6 +67,7 @@ export const Tree = ({ canLeaveParent, canAcceptChild, getItemChildren, + isItemHidden, renderItem, onSelect, onHover, @@ -77,7 +79,6 @@ export const Tree = ({ }: TreeProps) => { const { getIsExpanded, setIsExpanded } = useExpandState({ selectedItemSelector, - getItemChildren, }); const rootRef = useRef(null); @@ -112,6 +113,7 @@ export const Tree = ({ placementIndicator, getIsExpanded, getItemChildren, + isItemHidden, canAcceptChild, }); @@ -131,11 +133,7 @@ export const Tree = ({ 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 = ({ const keyboardNavigation = useKeyboardNavigation({ root, getItemChildren, + isItemHidden, selectedItemSelector, getIsExpanded, setIsExpanded, @@ -293,6 +292,7 @@ export const Tree = ({ ({ root, selectedItemSelector, getItemChildren, + isItemHidden, getIsExpanded, setIsExpanded, onEsc, @@ -331,6 +332,7 @@ const useKeyboardNavigation = ({ 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 = ({ 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 = ({ }; traverse([root.id]); return result; - }, [root, getIsExpanded, getItemChildren]); + }, [root, getIsExpanded, getItemChildren, isItemHidden]); const rootRef = useRef(null); @@ -465,12 +469,10 @@ const useKeyboardNavigation = ({ }; }; -const useExpandState = ({ +const useExpandState = ({ selectedItemSelector, - getItemChildren, }: { selectedItemSelector: undefined | ItemSelector; - getItemChildren: (itemId: ItemId) => Data[]; }) => { const [record, setRecord] = useState>({}); @@ -512,14 +514,9 @@ const useExpandState = ({ 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(